From 7c76020f4adcc6d7f504212d7df18ffb22bd7714 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 29 Nov 2019 16:27:49 -0800 Subject: [PATCH] Add feature to allow simple reversing of a for-loop --- .../CSharpFeaturesResources.Designer.cs | 9 + .../Portable/CSharpFeaturesResources.resx | 4 + ...erseForStatementCodeRefactoringProvider.cs | 365 ++++++++++++++++++ .../xlf/CSharpFeaturesResources.cs.xlf | 5 + .../xlf/CSharpFeaturesResources.de.xlf | 5 + .../xlf/CSharpFeaturesResources.es.xlf | 5 + .../xlf/CSharpFeaturesResources.fr.xlf | 5 + .../xlf/CSharpFeaturesResources.it.xlf | 5 + .../xlf/CSharpFeaturesResources.ja.xlf | 5 + .../xlf/CSharpFeaturesResources.ko.xlf | 5 + .../xlf/CSharpFeaturesResources.pl.xlf | 5 + .../xlf/CSharpFeaturesResources.pt-BR.xlf | 5 + .../xlf/CSharpFeaturesResources.ru.xlf | 5 + .../xlf/CSharpFeaturesResources.tr.xlf | 5 + .../xlf/CSharpFeaturesResources.zh-Hans.xlf | 5 + .../xlf/CSharpFeaturesResources.zh-Hant.xlf | 5 + 16 files changed, 443 insertions(+) create mode 100644 src/Features/CSharp/Portable/ReverseForStatement/CSharpReverseForStatementCodeRefactoringProvider.cs diff --git a/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs b/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs index d8f8ba450c8..c6dd0853355 100644 --- a/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs +++ b/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs @@ -1141,6 +1141,15 @@ internal class CSharpFeaturesResources { } } + /// + /// Looks up a localized string similar to Reverse 'for' statement. + /// + internal static string Reverse_for_statement { + get { + return ResourceManager.GetString("Reverse_for_statement", resourceCulture); + } + } + /// /// Looks up a localized string similar to select clause. /// diff --git a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx index f4c87ad990f..7a4ddbdd525 100644 --- a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx +++ b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx @@ -669,4 +669,8 @@ Pass in captured variables as arguments + + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + \ No newline at end of file diff --git a/src/Features/CSharp/Portable/ReverseForStatement/CSharpReverseForStatementCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ReverseForStatement/CSharpReverseForStatementCodeRefactoringProvider.cs new file mode 100644 index 00000000000..5bba7786725 --- /dev/null +++ b/src/Features/CSharp/Portable/ReverseForStatement/CSharpReverseForStatementCodeRefactoringProvider.cs @@ -0,0 +1,365 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.ReverseForStatement +{ + [ExportCodeRefactoringProvider(LanguageNames.CSharp), Shared] + internal class CSharpReverseForStatementCodeRefactoringProvider : CodeRefactoringProvider + { + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var forStatement = await context.TryGetRelevantNodeAsync().ConfigureAwait(false); + if (forStatement == null) + return; + + // We support the following cases + // + // for (var x = start; x < end ; x++) + // for (... ; ... ; ++x) + // for (... ; x <= end; ...) + // for (... ; ... ; x += 1) + // + // for (var x = end ; x >= start; x--) + // for (... ; ... ; --x) + // for (... ; ... ; x -= 1) + + var declaration = forStatement.Declaration; + if (declaration == null) + return; + + if (declaration.Variables.Count != 1) + return; + + if (forStatement.Incrementors.Count != 1) + return; + + var variable = declaration.Variables[0]; + var after = forStatement.Incrementors[0]; + + if (!(forStatement.Condition is BinaryExpressionSyntax condition)) + return; + + if (after == null) + return; + + var (document, _, cancellationToken) = context; + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (MatchesIncrementPattern(variable, condition, after, out _, out _, out _) || + MatchesDecrementPattern(variable, condition, after, out _, out _)) + { + context.RegisterRefactoring(new MyCodeAction( + c => ReverseForStatementAsync(document, forStatement, c))); + } + } + + private bool MatchesIncrementPattern( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax after, + out ExpressionSyntax start, out bool equals, out ExpressionSyntax end) + { + equals = default; + end = default; + return IsIncrementInitializer(variable, out start) && + IsIncrementCondition(variable, condition, out equals, out end) && + IsIncrementAfter(variable, after); + } + + private bool MatchesDecrementPattern( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax after, + out ExpressionSyntax end, out ExpressionSyntax start) + { + start = default; + return IsDecrementInitializer(variable, out end) && + IsDecrementCondition(variable, condition, out start) && + IsDecrementAfter(variable, after); + } + + private bool IsIncrementInitializer(VariableDeclaratorSyntax variable, out ExpressionSyntax start) + { + start = variable.Initializer?.Value; + return start != null; + } + + private bool IsIncrementCondition( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, + out bool equals, out ExpressionSyntax end) + { + // i < ... i <= ... + if (condition.Kind() == SyntaxKind.LessThanExpression || + condition.Kind() == SyntaxKind.LessThanOrEqualExpression) + { + end = condition.Right; + equals = condition.Kind() == SyntaxKind.LessThanOrEqualExpression; + return IsVariableReference(variable, condition.Left); + } + + // ... > i ... >= i + if (condition.Kind() == SyntaxKind.GreaterThanExpression || + condition.Kind() == SyntaxKind.GreaterThanOrEqualExpression) + { + end = condition.Left; + equals = condition.Kind() == SyntaxKind.GreaterThanOrEqualExpression; + return IsVariableReference(variable, condition.Right); + } + + end = default; + equals = default; + return false; + } + + private bool IsIncrementAfter( + VariableDeclaratorSyntax variable, ExpressionSyntax after) + { + // i++ + // ++i + // i += 1 + if (after is PostfixUnaryExpressionSyntax postfixUnary && + postfixUnary.Kind() == SyntaxKind.PostIncrementExpression && + IsVariableReference(variable, postfixUnary.Operand)) + { + return true; + } + + if (after is PrefixUnaryExpressionSyntax prefixUnary && + prefixUnary.Kind() == SyntaxKind.PreIncrementExpression && + IsVariableReference(variable, prefixUnary.Operand)) + { + return true; + } + + if (after is AssignmentExpressionSyntax assignment && + assignment.Kind() == SyntaxKind.AddAssignmentExpression && + IsVariableReference(variable, assignment.Left) && + IsLiteralOne(assignment.Right)) + { + return true; + } + + return false; + } + + private static bool IsLiteralOne(ExpressionSyntax expression) + => expression.WalkDownParentheses() is LiteralExpressionSyntax literal && literal.Token.Value is 1; + + private bool IsDecrementInitializer(VariableDeclaratorSyntax variable, out ExpressionSyntax end) + { + end = variable.Initializer?.Value; + return end != null; + } + + private bool IsDecrementCondition( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, + out ExpressionSyntax start) + { + // i >= ... + if (condition.Kind() == SyntaxKind.GreaterThanOrEqualExpression) + { + start = condition.Right; + return IsVariableReference(variable, condition.Left); + } + + // ... <= i + if (condition.Kind() == SyntaxKind.LessThanOrEqualExpression) + { + start = condition.Left; + return IsVariableReference(variable, condition.Right); + } + + start = default; + return false; + } + + private bool IsDecrementAfter( + VariableDeclaratorSyntax variable, ExpressionSyntax after) + { + // i-- + // --i + // i -= 1 + if (after is PostfixUnaryExpressionSyntax postfixUnary && + postfixUnary.Kind() == SyntaxKind.PostDecrementExpression && + IsVariableReference(variable, postfixUnary.Operand)) + { + return true; + } + + if (after is PrefixUnaryExpressionSyntax prefixUnary && + prefixUnary.Kind() == SyntaxKind.PreDecrementExpression && + IsVariableReference(variable, prefixUnary.Operand)) + { + return true; + } + + if (after is AssignmentExpressionSyntax assignment && + assignment.Kind() == SyntaxKind.SubtractAssignmentExpression && + IsVariableReference(variable, assignment.Left) && + IsLiteralOne(assignment.Right)) + { + return true; + } + + return false; + } + + private bool IsVariableReference(VariableDeclaratorSyntax variable, ExpressionSyntax expr) + => expr.WalkDownParentheses() is IdentifierNameSyntax identifier && + identifier.Identifier.ValueText == variable.Identifier.ValueText; + + private async Task ReverseForStatementAsync( + Document document, ForStatementSyntax forStatement, CancellationToken cancellationToken) + { + var variable = forStatement.Declaration.Variables[0]; + var condition = (BinaryExpressionSyntax)forStatement.Condition; + var after = forStatement.Incrementors[0]; + + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var editor = new SyntaxEditor(root, document.Project.Solution.Workspace); + var generator = editor.Generator; + if (MatchesIncrementPattern( + variable, condition, after, + out var start, out var equals, out var end)) + { + // for (var x = start ; x < end ; ...) => + // for (var x = end - 1; x >= start; ...) + // + // for (var x = start; x <= end ; ...) => + // for (var x = end ; x >= start; ...) => + + var newStart = equals + ? end + : (ExpressionSyntax)generator.SubtractExpression(end, generator.LiteralExpression(1)); + + editor.ReplaceNode(variable.Initializer.Value, Reduce(newStart)); + editor.ReplaceNode(condition, Reduce(Invert(variable, condition, start))); + } + else if (MatchesDecrementPattern( + variable, condition, after, + out end, out start)) + { + // for (var x = end; x >= start; x--) => + // for (var x = start; x <= end; x--) + editor.ReplaceNode(variable.Initializer.Value, Reduce(start)); + editor.ReplaceNode(condition, Reduce(Invert(variable, condition, end))); + } + else + { + throw new InvalidOperationException(); + } + + editor.ReplaceNode(after, InvertAfter(after)); + return document.WithSyntaxRoot(editor.GetChangedRoot()); + } + + private ExpressionSyntax Reduce(ExpressionSyntax expr) + { + expr = expr.WalkDownParentheses(); + + if (expr is BinaryExpressionSyntax outerBinary) + { + var reducedLeft = Reduce(outerBinary.Left); + var reducedRight = Reduce(outerBinary.Right); + + // (... + 1) - 1 => ... + // (... - 1) + 1 => ... + { + if (reducedLeft is BinaryExpressionSyntax innerLeft && + IsLiteralOne(innerLeft.Right) && + IsLiteralOne(reducedRight)) + { + if ((outerBinary.Kind() == SyntaxKind.SubtractExpression && innerLeft.Kind() == SyntaxKind.AddExpression) || + (outerBinary.Kind() == SyntaxKind.AddExpression && innerLeft.Kind() == SyntaxKind.SubtractExpression)) + { + return Reduce(innerLeft.Left); + } + } + } + + // v <= x - 1 => v < x + // x - 1 >= v => x > v + { + if (outerBinary.Kind() == SyntaxKind.LessThanOrEqualExpression && + reducedRight is BinaryExpressionSyntax innerRight && + innerRight.Kind() == SyntaxKind.SubtractExpression && + IsLiteralOne(innerRight.Right)) + { + var newOperator = SyntaxFactory.Token(SyntaxKind.LessThanToken).WithTriviaFrom(outerBinary.OperatorToken); + return Reduce(outerBinary.WithRight(innerRight.Left) + .WithOperatorToken(newOperator)); + } + + if (outerBinary.Kind() == SyntaxKind.GreaterThanOrEqualExpression && + reducedLeft is BinaryExpressionSyntax innerLeft && + innerLeft.Kind() == SyntaxKind.SubtractExpression && + IsLiteralOne(innerLeft.Right)) + { + var newOperator = SyntaxFactory.Token(SyntaxKind.GreaterThanToken).WithTriviaFrom(outerBinary.OperatorToken); + return Reduce(outerBinary.WithRight(innerLeft.Left) + .WithOperatorToken(newOperator)); + } + } + } + + return expr.WithAdditionalAnnotations(Formatter.Annotation); + } + + private BinaryExpressionSyntax Invert( + VariableDeclaratorSyntax variable, BinaryExpressionSyntax condition, ExpressionSyntax operand) + { + var (left, right) = IsVariableReference(variable, condition.Left) + ? (condition.Left, operand) + : (operand, condition.Right); + + var newOperatorKind = condition.Kind() == SyntaxKind.LessThanExpression || condition.Kind() == SyntaxKind.LessThanOrEqualExpression + ? SyntaxKind.GreaterThanEqualsToken + : SyntaxKind.LessThanEqualsToken; + + var newExpressionKind = newOperatorKind == SyntaxKind.GreaterThanEqualsToken + ? SyntaxKind.GreaterThanOrEqualExpression + : SyntaxKind.LessThanOrEqualExpression; + + var newOperator = SyntaxFactory.Token(newOperatorKind).WithTriviaFrom(condition.OperatorToken); + return SyntaxFactory.BinaryExpression(newExpressionKind, left, newOperator, right); + } + + private ExpressionSyntax InvertAfter(ExpressionSyntax after) + { + var opToken = after switch + { + PostfixUnaryExpressionSyntax postfixUnary => postfixUnary.OperatorToken, + PrefixUnaryExpressionSyntax prefixUnary => prefixUnary.OperatorToken, + AssignmentExpressionSyntax assignment => assignment.OperatorToken, + _ => throw ExceptionUtilities.UnexpectedValue(after.Kind()) + }; + + var newKind = opToken.Kind() switch + { + SyntaxKind.MinusMinusToken => SyntaxKind.PlusPlusToken, + SyntaxKind.PlusPlusToken => SyntaxKind.MinusMinusToken, + SyntaxKind.PlusEqualsToken => SyntaxKind.MinusEqualsToken, + SyntaxKind.MinusEqualsToken => SyntaxKind.PlusEqualsToken, + _ => throw ExceptionUtilities.UnexpectedValue(opToken.Kind()) + }; + + var newOpToken = SyntaxFactory.Token(newKind).WithTriviaFrom(opToken); + return after.ReplaceToken(opToken, newOpToken); + } + + private class MyCodeAction : CodeAction.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument) + : base(CSharpFeaturesResources.Reverse_for_statement, createChangedDocument) + { + } + } + } +} diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf index 2e76ed929bc..b11ccbfde43 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf @@ -137,6 +137,11 @@ Odebrat nepoužívané proměnné + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Zjednodušit výraz lambda diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf index 290829ff457..df6f5eda5aa 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf @@ -137,6 +137,11 @@ Nicht verwendete Variablen entfernen + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Lambdaausdruck vereinfachen diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf index ef44474fcc9..d35c8123713 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf @@ -137,6 +137,11 @@ Quitar variables no utilizadas + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Simplificar expresión lambda diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf index 329798614c4..2966445fe93 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf @@ -137,6 +137,11 @@ Supprimer les variables inutilisées + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Simplifier l'expression lambda diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf index ddf6bcfee82..1226c0b0fa7 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf @@ -137,6 +137,11 @@ Rimuovi le variabili non usate + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Semplifica l'espressione lambda diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf index 4cacf63271f..a79445ef8d2 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf @@ -137,6 +137,11 @@ 未使用の変数を削除する + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression ラムダ式の簡略化 diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf index 74ab767cb5b..89ac7329537 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf @@ -137,6 +137,11 @@ 사용하지 않는 변수 제거 + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression 람다 식 단순화 diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf index 6fcf313c413..9bd6753d6a1 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf @@ -137,6 +137,11 @@ Usuń nieużywane zmienne + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Uprość wyrażenie lambda diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf index 0de6dff804f..bc228467bbe 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf @@ -137,6 +137,11 @@ Remover variáveis não utilizadas + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Simplificar expressão lambda diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf index 79f0be83139..80f07852cb1 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf @@ -137,6 +137,11 @@ Удалить неиспользуемые переменные + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Упростить лямбда-выражение diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf index 1016a31a5ca..461f61f252d 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf @@ -137,6 +137,11 @@ Kullanılmayan değişkenleri kaldır + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression Lambda ifadesini basitleştir diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf index 86486e0cf27..89afabc8469 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf @@ -137,6 +137,11 @@ 删除未使用的变量 + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression 简化 lambda 表达式 diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf index 9cb08fb5b9a..47ccd78f1bb 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf @@ -137,6 +137,11 @@ 移除未使用的變數 + + Reverse 'for' statement + Reverse 'for' statement + {Locked="for"} "for" is a C# keyword and should not be localized. + Simplify lambda expression 簡化 Lambda 運算式 -- GitLab