Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

namespace Flow.Launcher.Localization.Analyzers.Localize
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]

Check warning on line 11 in Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

This compiler extension should not be implemented in an assembly containing a reference to Microsoft.CodeAnalysis.Workspaces. The Microsoft.CodeAnalysis.Workspaces assembly is not provided during command line compilation scenarios, so references to it could cause the compiler extension to behave unpredictably. (https://github.com/dotnet/roslyn-analyzers/blob/main/docs/rules/RS1038.md)

Check warning on line 11 in Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

This compiler extension should not be implemented in an assembly containing a reference to Microsoft.CodeAnalysis.Workspaces. The Microsoft.CodeAnalysis.Workspaces assembly is not provided during command line compilation scenarios, so references to it could cause the compiler extension to behave unpredictably. (https://github.com/dotnet/roslyn-analyzers/blob/main/docs/rules/RS1038.md)
public class OldGetTranslateAnalyzer : DiagnosticAnalyzer
{
#region DiagnosticAnalyzer

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
AnalyzerDiagnostics.OldLocalizationApiUsed
);
Expand All @@ -22,29 +24,42 @@
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;
Expand All @@ -59,27 +74,42 @@
}
}

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);
/// <summary>
/// Walk up the tree to see if we're already inside a Format call
/// </summary>
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) =>
Expand All @@ -92,5 +122,9 @@
return syntax.Token.ValueText;
return null;
}

#endregion

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> FixableDiagnosticIds => ImmutableArray.Create(
AnalyzerDiagnostics.OldLocalizationApiUsed.Id
);
Expand All @@ -39,44 +41,63 @@ 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;

if (root is null) return context.Document;

var invocationExpr = root
?.FindToken(diagnosticSpan.Start).Parent
?.AncestorsAndSelf()
.FindToken(diagnosticSpan.Start).Parent
.AncestorsAndSelf()
.OfType<InvocationExpressionSyntax>()
.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;
}

#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}()"
);
Expand All @@ -88,10 +109,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 == 1)
{
var firstArgument = invocationExpressionSyntax.ArgumentList.Arguments.First().Expression;
return GetTranslationKey(firstArgument);
Expand All @@ -104,13 +123,19 @@ private static Document FixOldTranslationWithStringFormat(
SeparatedSyntaxList<ArgumentSyntax> argumentList,
string translationKey2,
SyntaxNode root,
InvocationExpressionSyntax invocationExpr
) {
var newArguments = string.Join(", ", argumentList.Skip(1).Select(a => a.Expression));
InvocationExpressionSyntax invocationExpr,
int translationArgIndex)
{
// 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);
return context.Document.WithSyntaxRoot(newRoot);
}

#endregion

#endregion
}
}
Loading