From f630fc74cd9c2492101e4ac1d4c841446665bb03 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Tue, 11 Mar 2025 19:30:01 +0800 Subject: [PATCH 1/5] Support string.Format culture info parameter analyzer --- .../Localize/OldGetTranslateAnalyzer.cs | 86 +++++++++++++------ 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs index 6828749..4629ab5 100644 --- a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs +++ b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs @@ -11,6 +11,8 @@ namespace Flow.Launcher.Localization.Analyzers.Localize [DiagnosticAnalyzer(LanguageNames.CSharp)] public class OldGetTranslateAnalyzer : DiagnosticAnalyzer { + #region DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( AnalyzerDiagnostics.OldLocalizationApiUsed ); @@ -22,29 +24,42 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); } + #endregion + + #region Analyze Methods + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) { var invocationExpr = (InvocationExpressionSyntax)context.Node; var semanticModel = context.SemanticModel; var symbolInfo = semanticModel.GetSymbolInfo(invocationExpr); + // Check if the method is a format string call if (!(symbolInfo.Symbol is IMethodSymbol methodSymbol)) return; - if (IsFormatStringCall(methodSymbol) && - GetFirstArgumentInvocationExpression(invocationExpr) is InvocationExpressionSyntax innerInvocationExpr) + // First branch: detect a call to string.Format containing a translate call anywhere in its arguments. + if (IsFormatStringCall(methodSymbol)) { - if (!IsTranslateCall(semanticModel.GetSymbolInfo(innerInvocationExpr)) || - !(GetFirstArgumentStringValue(innerInvocationExpr) is string translationKey)) - return; - - var diagnostic = Diagnostic.Create( - AnalyzerDiagnostics.OldLocalizationApiUsed, - invocationExpr.GetLocation(), - translationKey, - GetInvocationArguments(invocationExpr) - ); - context.ReportDiagnostic(diagnostic); + var arguments = invocationExpr.ArgumentList.Arguments; + // Check all arguments is an invocation (i.e. a candidate for Context.API.GetTranslation(…)) + for (int i = 0; i < arguments.Count; i++) + { + if (GetArgumentInvocationExpression(invocationExpr, i) is InvocationExpressionSyntax innerInvocationExpr && + IsTranslateCall(semanticModel.GetSymbolInfo(innerInvocationExpr)) && + GetFirstArgumentStringValue(innerInvocationExpr) is string translationKey) + { + var diagnostic = Diagnostic.Create( + AnalyzerDiagnostics.OldLocalizationApiUsed, + invocationExpr.GetLocation(), + translationKey, + GetInvocationArguments(invocationExpr, i) + ); + context.ReportDiagnostic(diagnostic); + return; + } + } } + // Second branch: direct translate call (outside of a Format call) else if (IsTranslateCall(methodSymbol) && GetFirstArgumentStringValue(invocationExpr) is string translationKey) { if (IsParentFormatStringCall(semanticModel, invocationExpr)) return; @@ -59,27 +74,42 @@ private static void AnalyzeNode(SyntaxNodeAnalysisContext context) } } - private static string GetInvocationArguments(InvocationExpressionSyntax invocationExpr) => - string.Join(", ", invocationExpr.ArgumentList.Arguments.Skip(1)); + #region Utils - private static bool IsParentFormatStringCall(SemanticModel semanticModel, SyntaxNode syntaxNode) => - syntaxNode is InvocationExpressionSyntax invocationExpressionSyntax && - invocationExpressionSyntax.Parent?.Parent?.Parent is SyntaxNode parent && - IsFormatStringCall(semanticModel?.GetSymbolInfo(parent)); + private static string GetInvocationArguments(InvocationExpressionSyntax invocationExpr, int translateArgIndex) => + string.Join(", ", invocationExpr.ArgumentList.Arguments.Skip(translateArgIndex + 1)); - private static bool IsFormatStringCall(SymbolInfo? symbolInfo) => - symbolInfo is SymbolInfo info && IsFormatStringCall(info.Symbol as IMethodSymbol); + /// + /// Walk up the tree to see if we're already inside a Format call + /// + private static bool IsParentFormatStringCall(SemanticModel semanticModel, SyntaxNode syntaxNode) + { + var parent = syntaxNode.Parent; + while (parent != null) + { + if (parent is InvocationExpressionSyntax parentInvocation) + { + var symbol = semanticModel.GetSymbolInfo(parentInvocation).Symbol as IMethodSymbol; + if (IsFormatStringCall(symbol)) + { + return true; + } + } + parent = parent.Parent; + } + return false; + } private static bool IsFormatStringCall(IMethodSymbol methodSymbol) => - methodSymbol?.Name is Constants.StringFormatMethodName && - methodSymbol.ContainingType.ToDisplayString() is Constants.StringFormatTypeName; + methodSymbol?.Name == Constants.StringFormatMethodName && + methodSymbol.ContainingType.ToDisplayString() == Constants.StringFormatTypeName; - private static InvocationExpressionSyntax GetFirstArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr) => - invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression as InvocationExpressionSyntax; + private static InvocationExpressionSyntax GetArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr, int index) => + invocationExpr.ArgumentList.Arguments[index].Expression as InvocationExpressionSyntax; private static bool IsTranslateCall(SymbolInfo symbolInfo) => symbolInfo.Symbol is IMethodSymbol innerMethodSymbol && - innerMethodSymbol.Name is Constants.OldLocalizationMethodName && + innerMethodSymbol.Name == Constants.OldLocalizationMethodName && Constants.OldLocalizationClasses.Contains(innerMethodSymbol.ContainingType.Name); private static bool IsTranslateCall(IMethodSymbol methodSymbol) => @@ -92,5 +122,9 @@ private static string GetFirstArgumentStringValue(InvocationExpressionSyntax inv return syntax.Token.ValueText; return null; } + + #endregion + + #endregion } } From f12be6dc09b1fb8ca68c0666221b9a944a961c83 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Tue, 11 Mar 2025 19:32:32 +0800 Subject: [PATCH 2/5] Improve code quality --- .../OldGetTranslateAnalyzerCodeFixProvider.cs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs index 695cfe6..52f2ac1 100644 --- a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs +++ b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs @@ -14,6 +14,8 @@ namespace Flow.Launcher.Localization.Analyzers.Localize [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OldGetTranslateAnalyzerCodeFixProvider)), Shared] public class OldGetTranslateAnalyzerCodeFixProvider : CodeFixProvider { + #region CodeFixProvider + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( AnalyzerDiagnostics.OldLocalizationApiUsed.Id ); @@ -39,6 +41,10 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) ); } + #endregion + + #region Fix Methods + private static Document FixOldTranslation(CodeFixContext context, SyntaxNode root, Diagnostic diagnostic) { var diagnosticSpan = diagnostic.Location.SourceSpan; @@ -63,20 +69,19 @@ private static Document FixOldTranslation(CodeFixContext context, SyntaxNode roo return context.Document; } + #region Utils private static string GetTranslationKey(ExpressionSyntax syntax) { - if ( - syntax is LiteralExpressionSyntax literalExpressionSyntax && - literalExpressionSyntax.Token.Value is string translationKey - ) + if (syntax is LiteralExpressionSyntax literalExpressionSyntax && + literalExpressionSyntax.Token.Value is string translationKey) return translationKey; return null; } private static Document FixOldTranslationWithoutStringFormat( - CodeFixContext context, string translationKey, SyntaxNode root, InvocationExpressionSyntax invocationExpr - ) { + CodeFixContext context, string translationKey, SyntaxNode root, InvocationExpressionSyntax invocationExpr) + { var newInvocationExpr = SyntaxFactory.ParseExpression( $"{Constants.ClassName}.{translationKey}()" ); @@ -88,10 +93,8 @@ private static Document FixOldTranslationWithoutStringFormat( private static string GetTranslationKeyFromInnerInvocation(ExpressionSyntax syntax) { - if ( - syntax is InvocationExpressionSyntax invocationExpressionSyntax && - invocationExpressionSyntax.ArgumentList.Arguments.Count is 1 - ) + if (syntax is InvocationExpressionSyntax invocationExpressionSyntax && + invocationExpressionSyntax.ArgumentList.Arguments.Count is 1) { var firstArgument = invocationExpressionSyntax.ArgumentList.Arguments.First().Expression; return GetTranslationKey(firstArgument); @@ -104,13 +107,17 @@ private static Document FixOldTranslationWithStringFormat( SeparatedSyntaxList argumentList, string translationKey2, SyntaxNode root, - InvocationExpressionSyntax invocationExpr - ) { + InvocationExpressionSyntax invocationExpr) + { var newArguments = string.Join(", ", argumentList.Skip(1).Select(a => a.Expression)); var newInnerInvocationExpr = SyntaxFactory.ParseExpression($"{Constants.ClassName}.{translationKey2}({newArguments})"); var newRoot = root.ReplaceNode(invocationExpr, newInnerInvocationExpr); return context.Document.WithSyntaxRoot(newRoot); } + + #endregion + + #endregion } } From 8805539655ba8ab88cb81264e3e353f4e6eede19 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Tue, 11 Mar 2025 20:24:27 +0800 Subject: [PATCH 3/5] Support string.Format culture info parameter code fixer --- .../OldGetTranslateAnalyzerCodeFixProvider.cs | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs index 52f2ac1..e04329c 100644 --- a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs +++ b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs @@ -49,22 +49,38 @@ private static Document FixOldTranslation(CodeFixContext context, SyntaxNode roo { var diagnosticSpan = diagnostic.Location.SourceSpan; + if (root is null) return context.Document; + var invocationExpr = root ?.FindToken(diagnosticSpan.Start).Parent ?.AncestorsAndSelf() .OfType() - .First(); + .FirstOrDefault(); - if (invocationExpr is null || root is null) return context.Document; + if (invocationExpr is null) return context.Document; var argumentList = invocationExpr.ArgumentList.Arguments; - var argument = argumentList.First().Expression; - - if (GetTranslationKey(argument) is string translationKey) - return FixOldTranslationWithoutStringFormat(context, translationKey, root, invocationExpr); - if (GetTranslationKeyFromInnerInvocation(argument) is string translationKeyInside) - return FixOldTranslationWithStringFormat(context, argumentList, translationKeyInside, root, invocationExpr); + // Loop through the arguments to find the translation key. + for (int i = 0; i < argumentList.Count; i++) + { + var argument = argumentList[i].Expression; + + // Case 1: The argument is a literal (direct GetTranslation("key")) + if (GetTranslationKey(argument) is string translationKey) + return FixOldTranslationWithoutStringFormat(context, translationKey, root, invocationExpr); + + // Case 2: The argument is itself an invocation (nested GetTranslation) + if (GetTranslationKeyFromInnerInvocation(argument) is string translationKeyInside) + { + // If there are arguments following this translation call, treat as a Format call. + if (i < argumentList.Count - 1) + return FixOldTranslationWithStringFormat(context, argumentList, translationKeyInside, root, invocationExpr, i); + + // Otherwise, treat it as a direct translation call. + return FixOldTranslationWithoutStringFormat(context, translationKeyInside, root, invocationExpr); + } + } return context.Document; } @@ -94,7 +110,7 @@ private static Document FixOldTranslationWithoutStringFormat( private static string GetTranslationKeyFromInnerInvocation(ExpressionSyntax syntax) { if (syntax is InvocationExpressionSyntax invocationExpressionSyntax && - invocationExpressionSyntax.ArgumentList.Arguments.Count is 1) + invocationExpressionSyntax.ArgumentList.Arguments.Count == 1) { var firstArgument = invocationExpressionSyntax.ArgumentList.Arguments.First().Expression; return GetTranslationKey(firstArgument); @@ -107,9 +123,11 @@ private static Document FixOldTranslationWithStringFormat( SeparatedSyntaxList argumentList, string translationKey2, SyntaxNode root, - InvocationExpressionSyntax invocationExpr) + InvocationExpressionSyntax invocationExpr, + int translationArgIndex) { - var newArguments = string.Join(", ", argumentList.Skip(1).Select(a => a.Expression)); + // Skip all arguments before and including the translation call + var newArguments = string.Join(", ", argumentList.Skip(translationArgIndex + 1).Select(a => a.Expression)); var newInnerInvocationExpr = SyntaxFactory.ParseExpression($"{Constants.ClassName}.{translationKey2}({newArguments})"); var newRoot = root.ReplaceNode(invocationExpr, newInnerInvocationExpr); From 6cdf3fd2938f6b4f22b7a7cbcc4f9ddb82460def Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Tue, 11 Mar 2025 20:45:09 +0800 Subject: [PATCH 4/5] Improve code quality --- .../Localize/OldGetTranslateAnalyzerCodeFixProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs index e04329c..7e73b87 100644 --- a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs +++ b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs @@ -52,8 +52,8 @@ private static Document FixOldTranslation(CodeFixContext context, SyntaxNode roo if (root is null) return context.Document; var invocationExpr = root - ?.FindToken(diagnosticSpan.Start).Parent - ?.AncestorsAndSelf() + .FindToken(diagnosticSpan.Start).Parent + .AncestorsAndSelf() .OfType() .FirstOrDefault(); From 780eb9f8bded2f21aeda98fd2c62b32a155e95c3 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Tue, 11 Mar 2025 20:51:02 +0800 Subject: [PATCH 5/5] Fix little blank --- .../Localize/OldGetTranslateAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs index 4629ab5..9577a8c 100644 --- a/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs +++ b/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs @@ -105,7 +105,7 @@ private static bool IsFormatStringCall(IMethodSymbol methodSymbol) => methodSymbol.ContainingType.ToDisplayString() == Constants.StringFormatTypeName; private static InvocationExpressionSyntax GetArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr, int index) => - invocationExpr.ArgumentList.Arguments[index].Expression as InvocationExpressionSyntax; + invocationExpr.ArgumentList.Arguments[index].Expression as InvocationExpressionSyntax; private static bool IsTranslateCall(SymbolInfo symbolInfo) => symbolInfo.Symbol is IMethodSymbol innerMethodSymbol &&