From 90a4392e7a39407580d5f7ccbc8b6f3302ecc4a4 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 26 May 2024 04:40:11 +0600 Subject: [PATCH 1/4] Implement localization source generators and analyzers --- .../AnalyzerDiagnostics.cs | 51 ++ .../AnalyzerReleases.Shipped.md | 2 + .../AnalyzerReleases.Unshipped.md | 11 + .../Flow.Launcher.Analyzers.csproj | 20 + .../Localize/ContextAvailabilityAnalyzer.cs | 92 ++++ ...textAvailabilityAnalyzerCodeFixProvider.cs | 181 +++++++ .../Localize/OldGetTranslateAnalyzer.cs | 96 ++++ .../OldGetTranslateAnalyzerCodeFixProvider.cs | 102 ++++ .../AnalyzerReleases.Shipped.md | 2 + .../AnalyzerReleases.Unshipped.md | 13 + .../Flow.Launcher.SourceGenerators.csproj | 19 + .../Localize/LocalizeSourceGenerator.cs | 446 ++++++++++++++++++ .../SourceGeneratorDiagnostics.cs | 69 +++ Flow.Launcher.sln | 30 +- 14 files changed, 1133 insertions(+), 1 deletion(-) create mode 100644 Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs create mode 100644 Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj create mode 100644 Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs create mode 100644 Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs create mode 100644 Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs create mode 100644 Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs create mode 100644 Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md create mode 100644 Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md create mode 100644 Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj create mode 100644 Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs create mode 100644 Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs diff --git a/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs new file mode 100644 index 00000000000..de18299f81d --- /dev/null +++ b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs @@ -0,0 +1,51 @@ +using Microsoft.CodeAnalysis; + +namespace Flow.Launcher.Analyzers; + +public static class AnalyzerDiagnostics +{ + public static readonly DiagnosticDescriptor OldLocalizationApiUsed = new( + "FLAN0001", + "Old localization API used", + "Use `Localize.{0}({1})` instead", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextIsAField = new( + "FLAN0002", + "Plugin context is a field", + "Plugin context must be a static property instead", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextIsNotStatic = new( + "FLAN0003", + "Plugin context is not static", + "Plugin context must be a static property", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextAccessIsTooRestrictive = new( + "FLAN0004", + "Plugin context property access modifier is too restrictive", + "Plugin context property must be internal or public", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextIsNotDeclared = new( + "FLAN0005", + "Plugin context is not declared", + "Plugin context must be a static property of type `PluginInitContext`", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); +} diff --git a/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md b/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000000..5ccc9f037f6 --- /dev/null +++ b/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md b/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000000..d5f177c6c1a --- /dev/null +++ b/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,11 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +FLAN0001 | Localization | Warning | FLAN0001_OldLocalizationApiUsed +FLAN0002 | Localization | Error | FLAN0002_ContextIsAField +FLAN0003 | Localization | Error | FLAN0003_ContextIsNotStatic +FLAN0004 | Localization | Error | FLAN0004_ContextAccessIsTooRestrictive +FLAN0005 | Localization | Error | FLAN0005_ContextIsNotDeclared diff --git a/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj b/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj new file mode 100644 index 00000000000..3914cda733e --- /dev/null +++ b/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj @@ -0,0 +1,20 @@ + + + + 1.0.0 + net7.0 + enable + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs new file mode 100644 index 00000000000..f4082e7dc04 --- /dev/null +++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs @@ -0,0 +1,92 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Flow.Launcher.Analyzers.Localize; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class ContextAvailabilityAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + AnalyzerDiagnostics.ContextIsAField, + AnalyzerDiagnostics.ContextIsNotStatic, + AnalyzerDiagnostics.ContextAccessIsTooRestrictive, + AnalyzerDiagnostics.ContextIsNotDeclared + ); + + private const string PluginContextTypeName = "PluginInitContext"; + + private const string PluginInterfaceName = "IPluginI18n"; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); + } + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + var semanticModel = context.SemanticModel; + var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); + + if (!IsPluginEntryClass(classSymbol)) return; + + var contextProperty = classDeclaration.Members.OfType() + .Select(p => semanticModel.GetDeclaredSymbol(p)) + .FirstOrDefault(p => p is { Type.Name: PluginContextTypeName }); + + if (contextProperty is not null) + { + if (!contextProperty.IsStatic) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextIsNotStatic, + contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + )); + return; + } + + if (contextProperty.DeclaredAccessibility is Accessibility.Private or Accessibility.Protected) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextAccessIsTooRestrictive, + contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + )); + return; + } + + return; + } + + var fieldDeclaration = classDeclaration.Members + .OfType() + .SelectMany(f => f.Declaration.Variables) + .Select(f => semanticModel.GetDeclaredSymbol(f)) + .FirstOrDefault(f => f is IFieldSymbol { Type.Name: PluginContextTypeName }); + var parentSyntax = fieldDeclaration + ?.DeclaringSyntaxReferences[0] + .GetSyntax() + .FirstAncestorOrSelf(); + + if (parentSyntax is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextIsAField, + parentSyntax.GetLocation() + )); + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextIsNotDeclared, + classDeclaration.Identifier.GetLocation() + )); + } + + private static bool IsPluginEntryClass(INamedTypeSymbol? namedTypeSymbol) => + namedTypeSymbol?.Interfaces.Any(i => i.Name == PluginInterfaceName) ?? false; +} diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs new file mode 100644 index 00000000000..f9a398cd7ad --- /dev/null +++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs @@ -0,0 +1,181 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; + +namespace Flow.Launcher.Analyzers.Localize; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ContextAvailabilityAnalyzerCodeFixProvider)), Shared] +public class ContextAvailabilityAnalyzerCodeFixProvider : CodeFixProvider +{ + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( + AnalyzerDiagnostics.ContextIsAField.Id, + AnalyzerDiagnostics.ContextIsNotStatic.Id, + AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id, + AnalyzerDiagnostics.ContextIsNotDeclared.Id + ); + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + if (diagnostic.Id == AnalyzerDiagnostics.ContextIsAField.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with static property", + createChangedDocument: _ => Task.FromResult(FixContextIsAFieldError(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsAField.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotStatic.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Make static", + createChangedDocument: _ => Task.FromResult(FixContextIsNotStaticError(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsNotStatic.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Make internal", + createChangedDocument: _ => Task.FromResult(FixContextIsTooRestricted(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotDeclared.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Declare context property", + createChangedDocument: _ => Task.FromResult(FixContextNotDeclared(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsNotDeclared.Id + ), + diagnostic + ); + } + } + + private static MemberDeclarationSyntax? GetStaticContextPropertyDeclaration(string propertyName = "Context") => + SyntaxFactory.ParseMemberDeclaration( + $"internal static PluginInitContext {propertyName} {{ get; private set; }} = null!;" + ); + + private static Document GetFormattedDocument(CodeFixContext context, SyntaxNode root) + { + var formattedRoot = Formatter.Format( + root, + Formatter.Annotation, + context.Document.Project.Solution.Workspace + ); + + return context.Document.WithSyntaxRoot(formattedRoot); + } + + private Document FixContextNotDeclared(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) + { + var classDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (classDeclaration is not { BaseList: not null }) return context.Document; + + var newPropertyDeclaration = GetStaticContextPropertyDeclaration(); + if (newPropertyDeclaration is null) return context.Document; + + var annotatedNewPropertyDeclaration = newPropertyDeclaration + .WithLeadingTrivia(SyntaxFactory.ElasticLineFeed) + .WithTrailingTrivia(SyntaxFactory.ElasticLineFeed) + .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); + + var newMembers = classDeclaration.Members.Insert(0, annotatedNewPropertyDeclaration); + var newClassDeclaration = classDeclaration.WithMembers(newMembers); + + var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration); + + return GetFormattedDocument(context, newRoot); + } + + private static Document FixContextIsNotStaticError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) + { + var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (propertyDeclaration is null) return context.Document; + + var newPropertyDeclaration = propertyDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + + var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); + return context.Document.WithSyntaxRoot(newRoot); + } + + private static Document FixContextIsTooRestricted(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) + { + var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (propertyDeclaration is null) return context.Document; + + var newModifiers = SyntaxFactory.TokenList(); + foreach (var modifier in propertyDeclaration.Modifiers) + { + newModifiers = newModifiers.Add( + modifier.Kind() switch + { + SyntaxKind.PrivateKeyword or SyntaxKind.ProtectedKeyword => SyntaxFactory.Token( + SyntaxKind.InternalKeyword + ), + _ => modifier + } + ); + } + + var newPropertyDeclaration = propertyDeclaration.WithModifiers(newModifiers); + + var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); + return context.Document.WithSyntaxRoot(newRoot); + } + + private static Document FixContextIsAFieldError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) { + var fieldDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (fieldDeclaration is null) return context.Document; + + var field = fieldDeclaration.Declaration.Variables.First(); + var fieldIdentifier = field.Identifier.ToString(); + + var propertyDeclaration = GetStaticContextPropertyDeclaration(fieldIdentifier); + if (propertyDeclaration is null) return context.Document; + + var annotatedNewPropertyDeclaration = propertyDeclaration + .WithTriviaFrom(fieldDeclaration) + .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); + + var newRoot = root.ReplaceNode(fieldDeclaration, annotatedNewPropertyDeclaration); + + return GetFormattedDocument(context, newRoot); + } + + private static T? GetDeclarationSyntax(SyntaxNode root, TextSpan diagnosticSpan) where T : SyntaxNode => + root + .FindToken(diagnosticSpan.Start) + .Parent + ?.AncestorsAndSelf() + .OfType() + .First(); +} diff --git a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs new file mode 100644 index 00000000000..44f332330c9 --- /dev/null +++ b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs @@ -0,0 +1,96 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Flow.Launcher.Analyzers.Localize; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class OldGetTranslateAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed); + + private static readonly string[] oldLocalizationClasses = { "IPublicAPI", "Internationalization" }; + private const string OldLocalizationMethodName = "GetTranslation"; + + private const string StringFormatMethodName = "Format"; + private const string StringFormatTypeName = "string"; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var invocationExpr = (InvocationExpressionSyntax)context.Node; + var semanticModel = context.SemanticModel; + var symbolInfo = semanticModel.GetSymbolInfo(invocationExpr); + + if (symbolInfo.Symbol is not IMethodSymbol methodSymbol) return; + + if (IsFormatStringCall(methodSymbol) && + GetFirstArgumentInvocationExpression(invocationExpr) is {} innerInvocationExpr) + { + if (!IsTranslateCall(semanticModel.GetSymbolInfo(innerInvocationExpr)) || + GetFirstArgumentStringValue(innerInvocationExpr) is not {} translationKey) + return; + + var diagnostic = Diagnostic.Create( + AnalyzerDiagnostics.OldLocalizationApiUsed, + invocationExpr.GetLocation(), + translationKey, + GetInvocationArguments(invocationExpr) + ); + context.ReportDiagnostic(diagnostic); + } + else if (IsTranslateCall(methodSymbol) && GetFirstArgumentStringValue(invocationExpr) is {} translationKey) + { + if (IsParentFormatStringCall(semanticModel, invocationExpr)) return; + + var diagnostic = Diagnostic.Create( + AnalyzerDiagnostics.OldLocalizationApiUsed, + invocationExpr.GetLocation(), + translationKey, + string.Empty + ); + context.ReportDiagnostic(diagnostic); + } + } + + private static string GetInvocationArguments(InvocationExpressionSyntax invocationExpr) => + string.Join(", ", invocationExpr.ArgumentList.Arguments.Skip(1)); + + private static bool IsParentFormatStringCall(SemanticModel? semanticModel, SyntaxNode? syntaxNode) => + syntaxNode is InvocationExpressionSyntax { Parent.Parent.Parent: {} parent } && + IsFormatStringCall(semanticModel?.GetSymbolInfo(parent)); + + private static bool IsFormatStringCall(SymbolInfo? symbolInfo) => + symbolInfo is { Symbol: IMethodSymbol methodSymbol } && IsFormatStringCall(methodSymbol); + + private static bool IsFormatStringCall(IMethodSymbol? methodSymbol) => + methodSymbol is { Name: StringFormatMethodName } && + methodSymbol.ContainingType.ToDisplayString() is StringFormatTypeName; + + private static InvocationExpressionSyntax? GetFirstArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr) => + invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression as InvocationExpressionSyntax; + + private static bool IsTranslateCall(SymbolInfo symbolInfo) => + symbolInfo.Symbol is IMethodSymbol { Name: OldLocalizationMethodName } innerMethodSymbol && + oldLocalizationClasses.Contains(innerMethodSymbol.ContainingType.Name); + + private static bool IsTranslateCall(IMethodSymbol methodSymbol) => + methodSymbol is { Name: OldLocalizationMethodName } && + oldLocalizationClasses.Contains(methodSymbol.ContainingType.Name); + + private static string? GetFirstArgumentStringValue(InvocationExpressionSyntax invocationExpr) => + invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression switch + { + LiteralExpressionSyntax syntax => syntax.Token.ValueText, + _ => null + }; +} diff --git a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs new file mode 100644 index 00000000000..1742335883f --- /dev/null +++ b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs @@ -0,0 +1,102 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Flow.Launcher.Analyzers.Localize; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OldGetTranslateAnalyzerCodeFixProvider)), Shared] +public class OldGetTranslateAnalyzerCodeFixProvider : CodeFixProvider +{ + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed.Id); + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics.First(); + + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with 'Localize.localization_key(...args)'", + createChangedDocument: _ => Task.FromResult(FixOldTranslation(context, root, diagnostic)), + equivalenceKey: AnalyzerDiagnostics.OldLocalizationApiUsed.Id + ), + diagnostic + ); + } + + private static Document FixOldTranslation(CodeFixContext context, SyntaxNode? root, Diagnostic diagnostic) + { + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var invocationExpr = root + ?.FindToken(diagnosticSpan.Start).Parent + ?.AncestorsAndSelf() + .OfType() + .First(); + + if (invocationExpr is null || root is null) return context.Document; + + var argumentList = invocationExpr.ArgumentList.Arguments; + var argument = argumentList.First().Expression; + + if (GetTranslationKey(argument) is { } translationKey) + return FixOldTranslationWithoutStringFormat(context, translationKey, root, invocationExpr); + + if (GetTranslationKeyFromInnerInvocation(argument) is { } translationKeyInside) + return FixOldTranslationWithStringFormat(context, argumentList, translationKeyInside, root, invocationExpr); + + return context.Document; + } + + + private static string? GetTranslationKey(ExpressionSyntax syntax) => + syntax switch + { + LiteralExpressionSyntax { Token.Value: string translationKey } => translationKey, + _ => null + }; + private static Document FixOldTranslationWithoutStringFormat( + CodeFixContext context, string translationKey, SyntaxNode root, InvocationExpressionSyntax invocationExpr + ) { + var newInvocationExpr = SyntaxFactory.ParseExpression( + $"Localize.{translationKey}()" + ); + + var newRoot = root.ReplaceNode(invocationExpr, newInvocationExpr); + var newDocument = context.Document.WithSyntaxRoot(newRoot); + return newDocument; + } + + private static string? GetTranslationKeyFromInnerInvocation(ExpressionSyntax syntax) => + syntax switch + { + InvocationExpressionSyntax { ArgumentList.Arguments: { Count: 1 } arguments } => + GetTranslationKey(arguments.First().Expression), + _ => null + }; + + private static Document FixOldTranslationWithStringFormat( + CodeFixContext context, + SeparatedSyntaxList argumentList, + string translationKey2, + SyntaxNode root, + InvocationExpressionSyntax invocationExpr + ) { + var newArguments = string.Join(", ", argumentList.Skip(1).Select(a => a.Expression)); + var newInnerInvocationExpr = SyntaxFactory.ParseExpression($"Localize.{translationKey2}({newArguments})"); + + var newRoot = root.ReplaceNode(invocationExpr, newInnerInvocationExpr); + return context.Document.WithSyntaxRoot(newRoot); + } + +} diff --git a/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000000..5ccc9f037f6 --- /dev/null +++ b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000000..f60d25850da --- /dev/null +++ b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,13 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +FLSG0001 | Localization | Warning | FLSG0001_CouldNotFindResourceDictionaries +FLSG0002 | Localization | Warning | FLSG0002_CouldNotFindPluginEntryClass +FLSG0003 | Localization | Warning | FLSG0003_CouldNotFindContextProperty +FLSG0004 | Localization | Warning | FLSG0004_ContextPropertyNotStatic +FLSG0005 | Localization | Warning | FLSG0005_ContextPropertyIsPrivate +FLSG0006 | Localization | Warning | FLSG0006_ContextPropertyIsProtected +FLSG0007 | Localization | Warning | FLSG0007_LocalizationKeyUnused diff --git a/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj b/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj new file mode 100644 index 00000000000..e192c04dcc7 --- /dev/null +++ b/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj @@ -0,0 +1,19 @@ + + + + 1.0.0 + net7.0 + enable + enable + true + Flow.Launcher.SourceGenerators + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs new file mode 100644 index 00000000000..9c984d5b9b7 --- /dev/null +++ b/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs @@ -0,0 +1,446 @@ +using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Flow.Launcher.SourceGenerators.Localize; + +[Generator] +public partial class LocalizeSourceGenerator : ISourceGenerator +{ + private OptimizationLevel _optimizationLevel; + + private const string CoreNamespace1 = "Flow.Launcher"; + private const string CoreNamespace2 = "Flow.Launcher.Core"; + private const string DefaultNamespace = "Flow.Launcher"; + private const string ClassName = "Localize"; + private const string PluginInterfaceName = "IPluginI18n"; + private const string PluginContextTypeName = "PluginInitContext"; + private const string KeywordStatic = "static"; + private const string KeywordPrivate = "private"; + private const string KeywordProtected = "protected"; + private const string XamlPrefix = "system"; + private const string XamlTag = "String"; + + private const string DefaultLanguageFilePathEndsWith = @"\Languages\en.xaml"; + private const string XamlCustomPathPropertyKey = "build_property.localizegeneratorlangfiles"; + private readonly char[] _xamlCustomPathPropertyDelimiters = { '\n', ';' }; + private readonly Regex _languagesXamlRegex = RegexEndsWithLanguagesXaml(); + + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + _optimizationLevel = context.Compilation.Options.OptimizationLevel; + + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( + XamlCustomPathPropertyKey, + out var langFilePathEndsWithStr + ); + + var allLanguageKeys = new HashSet(); + context.Compilation.SyntaxTrees + .SelectMany(v => v.GetRoot().DescendantNodes().OfType()) + .ToList() + .ForEach( + v => + { + var split = v.Expression.ToString().Split('.'); + if (split.Length < 2) return; + if (split[0] is not ClassName) return; + allLanguageKeys.Add(split[1]); + }); + + var allXamlFiles = context.AdditionalFiles + .Where(v => _languagesXamlRegex.IsMatch(v.Path)) + .ToArray(); + AdditionalText[] resourceDictionaries; + if (allXamlFiles.Length is 0) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, + Location.None + )); + return; + } + + if (string.IsNullOrEmpty(langFilePathEndsWithStr)) + { + if (allXamlFiles.Length is 1) + { + resourceDictionaries = allXamlFiles; + } + else + { + resourceDictionaries = allXamlFiles.Where(v => v.Path.EndsWith(DefaultLanguageFilePathEndsWith)).ToArray(); + if (resourceDictionaries.Length is 0) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, + Location.None + )); + return; + } + } + } + else + { + var langFilePathEndings = langFilePathEndsWithStr + .Trim() + .Split(_xamlCustomPathPropertyDelimiters) + .Select(v => v.Trim()) + .ToArray(); + resourceDictionaries = allXamlFiles.Where(v => langFilePathEndings.Any(v.Path.EndsWith)).ToArray(); + if (resourceDictionaries.Length is 0) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, + Location.None + )); + return; + } + } + + var ns = context.Compilation.AssemblyName ?? DefaultNamespace; + + var localizedStrings = LoadLocalizedStrings(resourceDictionaries); + + var unusedLocalizationKeys = localizedStrings.Keys.Except(allLanguageKeys).ToHashSet(); + + foreach (var key in unusedLocalizationKeys) + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.LocalizationKeyUnused, + Location.None, + key + )); + + var sourceCode = GenerateSourceCode(localizedStrings, context, unusedLocalizationKeys); + + context.AddSource($"{ClassName}.{ns}.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); + } + + private static Dictionary LoadLocalizedStrings(IEnumerable files) + { + var result = new Dictionary(); + + foreach (var file in files) + { + ProcessXamlFile(file, result); + } + + return result; + } + + private static void ProcessXamlFile(AdditionalText file, Dictionary result) { + var content = file.GetText()?.ToString(); + if (content is null) return; + var doc = XDocument.Parse(content); + var ns = doc.Root?.GetNamespaceOfPrefix(XamlPrefix); + if (ns is null) return; + foreach (var element in doc.Descendants(ns + XamlTag)) + { + var name = element.FirstAttribute?.Value; + var value = element.Value; + + if (name is null) continue; + + string? summary = null; + var paramsList = new List(); + var commentNode = element.PreviousNode; + + if (commentNode is XComment comment) + summary = ProcessXamlFileComment(comment, paramsList); + + result[name] = new LocalizableString(name, value, summary, paramsList); + } + } + + private static string? ProcessXamlFileComment(XComment comment, List paramsList) { + string? summary = null; + try + { + if (CommentIncludesDocumentationMarkup(comment)) + { + var commentDoc = XDocument.Parse($"{comment.Value}"); + summary = ExtractDocumentationCommentSummary(commentDoc); + foreach (var param in commentDoc.Descendants("param")) + { + var index = int.Parse(param.Attribute("index")?.Value ?? "-1"); + var paramName = param.Attribute("name")?.Value; + var paramType = param.Attribute("type")?.Value; + if (index < 0 || paramName is null || paramType is null) continue; + paramsList.Add(new LocalizableStringParam(index, paramName, paramType)); + } + } + } + catch + { + // ignore + } + + return summary; + } + + private static string? ExtractDocumentationCommentSummary(XDocument commentDoc) { + return commentDoc.Descendants("summary").FirstOrDefault()?.Value.Trim(); + } + + private static bool CommentIncludesDocumentationMarkup(XComment comment) { + return comment.Value.Contains("", StringComparison.OrdinalIgnoreCase) || + comment.Value.Contains(" localizedStrings, + GeneratorExecutionContext context, + HashSet unusedLocalizationKeys + ) + { + var ns = context.Compilation.AssemblyName; + + var sb = new StringBuilder(); + if (ns is CoreNamespace1 or CoreNamespace2) + { + GenerateFileHeader(sb, context); + GenerateClass(sb, localizedStrings, unusedLocalizationKeys); + return sb.ToString(); + } + + string? contextPropertyName = null; + var mainClassFound = false; + foreach (var (syntaxTree, classDeclaration) in GetClasses(context)) + { + if (!DoesClassImplementInterface(classDeclaration, PluginInterfaceName)) + continue; + + mainClassFound = true; + + var property = GetPluginContextProperty(classDeclaration); + if (property is null) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindContextProperty, + GetLocation(syntaxTree, classDeclaration), + classDeclaration.Identifier + )); + return string.Empty; + } + + var propertyModifiers = GetPropertyModifiers(property); + + if (!propertyModifiers.Static) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyNotStatic, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } + + if (propertyModifiers.Private) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyIsPrivate, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } + + if (propertyModifiers.Protected) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyIsProtected, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } + + contextPropertyName = $"{classDeclaration.Identifier}.{property.Identifier}"; + break; + } + + if (mainClassFound is false) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass, + Location.None + )); + return string.Empty; + } + + GenerateFileHeader(sb, context, true); + GenerateClass(sb, localizedStrings, unusedLocalizationKeys, contextPropertyName); + return sb.ToString(); + } + + private static void GenerateFileHeader(StringBuilder sb, GeneratorExecutionContext context, bool isPlugin = false) + { + var rootNamespace = context.Compilation.AssemblyName; + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + + if (!isPlugin) + sb.AppendLine("using Flow.Launcher.Core.Resource;"); + + sb.AppendLine($"namespace {rootNamespace};"); + } + + private void GenerateClass( + StringBuilder sb, + Dictionary localizedStrings, + HashSet unusedLocalizationKeys, + string? propertyName = null + ) + { + sb.AppendLine(); + sb.AppendLine($"public static class {ClassName}"); + sb.AppendLine("{"); + foreach (var localizedString in localizedStrings) + { + if (_optimizationLevel == OptimizationLevel.Release && unusedLocalizationKeys.Contains(localizedString.Key)) + continue; + + GenerateDocCommentForMethod(sb, localizedString.Value); + GenerateMethod(sb, localizedString.Value, propertyName); + } + + sb.AppendLine("}"); + } + + private static void GenerateDocCommentForMethod(StringBuilder sb, LocalizableString localizableString) + { + sb.AppendLine("/// "); + if (localizableString.Summary is not null) + { + sb.AppendLine(string.Join('\n', localizableString.Summary.Trim().Split("\n").Select(v => $"/// {v}"))); + } + + sb.AppendLine("/// "); + var value = localizableString.Value; + foreach (var p in localizableString.Params) + { + value = value.Replace($"{{{p.Index}}}", $"{{{p.Name}}}"); + } + sb.AppendLine(string.Join('\n', value.Split("\n").Select(v => $"/// {v}"))); + sb.AppendLine("/// "); + sb.AppendLine("/// "); + } + + private static void GenerateMethod(StringBuilder sb, LocalizableString localizableString, string? contextPropertyName) + { + sb.Append($"public static string {localizableString.Key}("); + var declarationArgs = new List(); + var callArgs = new List(); + for (var i = 0; i < 10; i++) + { + if (localizableString.Value.Contains($"{{{i}}}")) + { + var param = localizableString.Params.FirstOrDefault(v => v.Index == i); + if (param is not null) + { + declarationArgs.Add($"{param.Type} {param.Name}"); + callArgs.Add(param.Name); + } + else + { + declarationArgs.Add($"object? arg{i}"); + callArgs.Add($"arg{i}"); + } + } + else + { + break; + } + } + + var callArray = callArgs.Count switch + { + 0 => "", + 1 => callArgs[0], + _ => $"new object?[] {{ {string.Join(", ", callArgs)} }}" + }; + + sb.Append(string.Join(", ", declarationArgs)); + sb.Append(") => "); + if (contextPropertyName is null) + { + if (string.IsNullOrEmpty(callArray)) + { + sb.AppendLine($"""InternationalizationManager.Instance.GetTranslation("{localizableString.Key}");"""); + } + else + { + sb.AppendLine( + $"""string.Format(InternationalizationManager.Instance.GetTranslation("{localizableString.Key}"), {callArray});""" + ); + } + } + else + { + if (string.IsNullOrEmpty(callArray)) + { + sb.AppendLine($"""{contextPropertyName}.API.GetTranslation("{localizableString.Key}");"""); + } + else + { + sb.AppendLine($"""string.Format({contextPropertyName}.API.GetTranslation("{localizableString.Key}"), {callArray});"""); + } + } + + sb.AppendLine(); + } + + private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration) + { + return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan); + } + + private static IEnumerable<(SyntaxTree, ClassDeclarationSyntax)> GetClasses(GeneratorExecutionContext context) + { + foreach (var syntaxTree in context.Compilation.SyntaxTrees) + { + var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType(); + foreach (var classDeclaration in classDeclarations) + { + yield return (syntaxTree, classDeclaration); + } + } + } + + private static bool DoesClassImplementInterface(ClassDeclarationSyntax classDeclaration, string interfaceName) + { + return classDeclaration.BaseList?.Types.Any(v => interfaceName == v.ToString()) is true; + } + + private static PropertyDeclarationSyntax? GetPluginContextProperty(ClassDeclarationSyntax classDeclaration) + { + return classDeclaration.Members + .OfType() + .FirstOrDefault(v => v.Type.ToString() is PluginContextTypeName); + } + + private static Modifiers GetPropertyModifiers(PropertyDeclarationSyntax property) + { + var isStatic = property.Modifiers.Any(v => v.Text is KeywordStatic); + var isPrivate = property.Modifiers.Any(v => v.Text is KeywordPrivate); + var isProtected = property.Modifiers.Any(v => v.Text is KeywordProtected); + + return new Modifiers(isStatic, isPrivate, isProtected); + } + + private record Modifiers(bool Static = false, bool Private = false, bool Protected = false); + + [GeneratedRegex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase)] + private static partial Regex RegexEndsWithLanguagesXaml(); +} + +public record LocalizableString(string Key, string Value, string? Summary, IEnumerable Params); +public record LocalizableStringParam(int Index, string Name, string Type); diff --git a/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs b/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs new file mode 100644 index 00000000000..ba7992dbca4 --- /dev/null +++ b/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs @@ -0,0 +1,69 @@ +using Microsoft.CodeAnalysis; + +namespace Flow.Launcher.SourceGenerators; + +public static class SourceGeneratorDiagnostics +{ + public static readonly DiagnosticDescriptor CouldNotFindResourceDictionaries = new( + "FLSG0001", + "Could not find resource dictionaries", + "Could not find resource dictionaries. There must be a file named [LANG].xaml file (for example, en.xaml), and it must be specified in in your .csproj file.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor CouldNotFindPluginEntryClass = new( + "FLSG0002", + "Could not find the main class of plugin", + "Could not find the main class of your plugin. It must implement IPluginI18n.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor CouldNotFindContextProperty = new( + "FLSG0003", + "Could not find plugin context property", + "Could not find a property of type PluginInitContext in {0}. It must be a public static or internal static property of the main class of your plugin.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextPropertyNotStatic = new( + "FLSG0004", + "Plugin context property is not static", + "Context property {0} is not static. It must be static.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextPropertyIsPrivate = new( + "FLSG0005", + "Plugin context property is private", + "Context property {0} is private. It must be either internal or public.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextPropertyIsProtected = new( + "FLSG0006", + "Plugin context property is protected", + "Context property {0} is protected. It must be either internal or public.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor LocalizationKeyUnused = new( + "FLSG0007", + "Localization key is unused", + "Method 'Localize.{0}' is never used", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); +} diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln index e44b23232fb..bdbcdc4d7db 100644 --- a/Flow.Launcher.sln +++ b/Flow.Launcher.sln @@ -71,6 +71,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Plugin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.WindowsSettings", "Plugins\Flow.Launcher.Plugin.WindowsSettings\Flow.Launcher.Plugin.WindowsSettings.csproj", "{5043CECE-E6A7-4867-9CBE-02D27D83747A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.SourceGenerators", "Flow.Launcher.SourceGenerators\Flow.Launcher.SourceGenerators.csproj", "{2CB3B152-6147-48E2-B497-CEFA3331E9B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Analyzers", "Flow.Launcher.Analyzers\Flow.Launcher.Analyzers.csproj", "{58A54F2B-772F-4724-861D-F3A191441DF1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,7 +86,7 @@ Global EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -286,6 +290,30 @@ Global {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.Build.0 = Release|Any CPU {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.ActiveCfg = Release|Any CPU {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.Build.0 = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x64.ActiveCfg = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x64.Build.0 = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x86.ActiveCfg = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x86.Build.0 = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|Any CPU.Build.0 = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x64.ActiveCfg = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x64.Build.0 = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x86.ActiveCfg = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x86.Build.0 = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x64.ActiveCfg = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x64.Build.0 = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x86.ActiveCfg = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x86.Build.0 = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|Any CPU.Build.0 = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x64.ActiveCfg = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x64.Build.0 = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x86.ActiveCfg = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 52435b704e556197277b33219bf087d8383900d5 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 26 May 2024 04:40:56 +0600 Subject: [PATCH 2/4] Add demo of localization source generators and analyzers --- Flow.Launcher.Core/Flow.Launcher.Core.csproj | 26 +++++++++++---- Flow.Launcher.Core/Resource/Theme.cs | 14 ++++---- Flow.Launcher.Core/Updater.cs | 33 +++++++------------ Flow.Launcher/Languages/en.xaml | 5 +++ ...low.Launcher.Plugin.BrowserBookmark.csproj | 14 +++++++- .../Flow.Launcher.Plugin.Calculator.csproj | 24 ++++++++++---- 6 files changed, 74 insertions(+), 42 deletions(-) diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 18101ccf04e..c7c9193df82 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -13,7 +13,7 @@ false en - + true portable @@ -24,7 +24,7 @@ 4 false - + pdbonly true @@ -39,11 +39,11 @@ - + - + @@ -51,7 +51,7 @@ - + @@ -60,10 +60,22 @@ - + - \ No newline at end of file + + + + + + + diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 96338cf6a1a..feeb978b328 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -79,14 +79,14 @@ public bool ChangeTheme(string theme) { if (string.IsNullOrEmpty(path)) throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); - + // reload all resources even if the theme itself hasn't changed in order to pickup changes // to things like fonts UpdateResourceDictionary(GetResourceDictionary(theme)); - + Settings.Theme = theme; - + //always allow re-loading default theme, in case of failure of switching to a new theme from default theme if (_oldTheme != theme || theme == defaultTheme) { @@ -105,7 +105,7 @@ public bool ChangeTheme(string theme) Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); if (theme != defaultTheme) { - MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme)); + MessageBox.Show(Localize.theme_load_failure_path_not_exists(theme)); ChangeTheme(defaultTheme); } return false; @@ -115,7 +115,7 @@ public bool ChangeTheme(string theme) Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); if (theme != defaultTheme) { - MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme)); + MessageBox.Show(Localize.theme_load_failure_parse_error(theme)); ChangeTheme(defaultTheme); } return false; @@ -148,7 +148,7 @@ private ResourceDictionary GetThemeResourceDictionary(string theme) public ResourceDictionary GetResourceDictionary(string theme) { var dict = GetThemeResourceDictionary(theme); - + if (dict["QueryBoxStyle"] is Style queryBoxStyle && dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) { @@ -189,7 +189,7 @@ public ResourceDictionary GetResourceDictionary(string theme) Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; Array.ForEach( - new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o + new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); } /* Ignore Theme Window Width and use setting */ diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 3f64b273e4c..c197347d23a 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -37,8 +37,7 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) try { if (!silentUpdate) - api.ShowMsg(api.GetTranslation("pleaseWait"), - api.GetTranslation("update_flowlauncher_update_check")); + api.ShowMsg(Localize.pleaseWait(), Localize.update_flowlauncher_update_check()); using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false); @@ -53,13 +52,12 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) if (newReleaseVersion <= currentVersion) { if (!silentUpdate) - MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest")); + MessageBox.Show(Localize.update_flowlauncher_already_on_latest()); return; } if (!silentUpdate) - api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), - api.GetTranslation("update_flowlauncher_updating")); + api.ShowMsg(Localize.update_flowlauncher_update_found(), Localize.update_flowlauncher_updating()); await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); @@ -70,20 +68,22 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) - MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), - DataLocation.PortableDataPath, - targetDestination)); + MessageBox.Show( + Localize.update_flowlauncher_fail_moving_portable_user_profile_data( + DataLocation.PortableDataPath, targetDestination + ) + ); } else { await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false); } - var newVersionTips = NewVersionTips(newReleaseVersion.ToString()); + var newVersionTips = Localize.newVersionTips(newReleaseVersion.ToString()); Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); - if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) + if (MessageBox.Show(newVersionTips, Localize.update_flowlauncher_new_update(), MessageBoxButton.YesNo) == MessageBoxResult.Yes) { UpdateManager.RestartApp(Constant.ApplicationFileName); } @@ -94,10 +94,9 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); else Log.Exception($"|Updater.UpdateApp|Error Occurred", e); - + if (!silentUpdate) - api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), - api.GetTranslation("update_flowlauncher_check_connection")); + api.ShowMsg(Localize.update_flowlauncher_fail(), Localize.update_flowlauncher_check_connection()); } finally { @@ -140,13 +139,5 @@ private async Task GitHubUpdateManagerAsync(string repository) return manager; } - - public string NewVersionTips(string version) - { - var translator = InternationalizationManager.Instance; - var tips = string.Format(translator.GetTranslation("newVersionTips"), version); - - return tips; - } } } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 070c290cdfb..cab0da722c1 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -146,7 +146,9 @@ Result Item Font Window Mode Opacity + Theme {0} not exists, fallback to default theme + Fail to load theme {0}, fallback to default theme Theme Folder Open Theme Folder @@ -254,6 +256,7 @@ You have activated Flow Launcher {0} times Check for Updates Become A Sponsor + New version {0} is available, would you like to restart Flow Launcher to use the update? Check updates failed, please check your connection and proxy settings to api.github.com. @@ -358,6 +361,8 @@ You already have the latest Flow Launcher version Update found Updating... + Flow Launcher was not able to move your user profile data to the new update version. Please manually move your profile data folder from {0} to {1} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index fe118c2c3bf..0ce9d6d458f 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -59,4 +59,16 @@ - \ No newline at end of file + + + + + + + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj index 415f852f4c8..81170d95f85 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj @@ -13,7 +13,7 @@ false en - + true portable @@ -24,7 +24,7 @@ 4 false - + pdbonly true @@ -34,13 +34,13 @@ 4 false - + PreserveNewest - + @@ -64,5 +64,17 @@ - - \ No newline at end of file + + + + + + + + From 4f24b729ddc31294cc0a62e85aa76d4007c3635f Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 26 May 2024 09:33:26 +0600 Subject: [PATCH 3/4] Modify analyzers and source generators to use .NET Standard 2.0 --- .../AnalyzerDiagnostics.cs | 87 +-- .../Flow.Launcher.Analyzers.csproj | 4 +- .../Localize/ContextAvailabilityAnalyzer.cs | 126 ++-- ...textAvailabilityAnalyzerCodeFixProvider.cs | 274 +++---- .../Localize/OldGetTranslateAnalyzer.cs | 146 ++-- .../OldGetTranslateAnalyzerCodeFixProvider.cs | 168 +++-- .../Flow.Launcher.SourceGenerators.csproj | 4 +- .../Localize/LocalizeSourceGenerator.cs | 699 ++++++++++-------- .../SourceGeneratorDiagnostics.cs | 119 +-- 9 files changed, 845 insertions(+), 782 deletions(-) diff --git a/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs index de18299f81d..b593a23fe17 100644 --- a/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs +++ b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs @@ -1,51 +1,52 @@ using Microsoft.CodeAnalysis; -namespace Flow.Launcher.Analyzers; - -public static class AnalyzerDiagnostics +namespace Flow.Launcher.Analyzers { - public static readonly DiagnosticDescriptor OldLocalizationApiUsed = new( - "FLAN0001", - "Old localization API used", - "Use `Localize.{0}({1})` instead", - "Localization", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static class AnalyzerDiagnostics + { + public static readonly DiagnosticDescriptor OldLocalizationApiUsed = new DiagnosticDescriptor( + "FLAN0001", + "Old localization API used", + "Use `Localize.{0}({1})` instead", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor ContextIsAField = new( - "FLAN0002", - "Plugin context is a field", - "Plugin context must be a static property instead", - "Localization", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor ContextIsAField = new DiagnosticDescriptor( + "FLAN0002", + "Plugin context is a field", + "Plugin context must be a static property instead", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor ContextIsNotStatic = new( - "FLAN0003", - "Plugin context is not static", - "Plugin context must be a static property", - "Localization", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor ContextIsNotStatic = new DiagnosticDescriptor( + "FLAN0003", + "Plugin context is not static", + "Plugin context must be a static property", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor ContextAccessIsTooRestrictive = new( - "FLAN0004", - "Plugin context property access modifier is too restrictive", - "Plugin context property must be internal or public", - "Localization", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor ContextAccessIsTooRestrictive = new DiagnosticDescriptor( + "FLAN0004", + "Plugin context property access modifier is too restrictive", + "Plugin context property must be internal or public", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor ContextIsNotDeclared = new( - "FLAN0005", - "Plugin context is not declared", - "Plugin context must be a static property of type `PluginInitContext`", - "Localization", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor ContextIsNotDeclared = new DiagnosticDescriptor( + "FLAN0005", + "Plugin context is not declared", + "Plugin context must be a static property of type `PluginInitContext`", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + } } diff --git a/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj b/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj index 3914cda733e..231d8d1afdd 100644 --- a/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj +++ b/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj @@ -2,9 +2,7 @@ 1.0.0 - net7.0 - enable - enable + netstandard2.0 true diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs index f4082e7dc04..eede68bfe07 100644 --- a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs +++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs @@ -1,92 +1,94 @@ using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Flow.Launcher.Analyzers.Localize; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ContextAvailabilityAnalyzer : DiagnosticAnalyzer +namespace Flow.Launcher.Analyzers.Localize { - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - AnalyzerDiagnostics.ContextIsAField, - AnalyzerDiagnostics.ContextIsNotStatic, - AnalyzerDiagnostics.ContextAccessIsTooRestrictive, - AnalyzerDiagnostics.ContextIsNotDeclared - ); + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ContextAvailabilityAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + AnalyzerDiagnostics.ContextIsAField, + AnalyzerDiagnostics.ContextIsNotStatic, + AnalyzerDiagnostics.ContextAccessIsTooRestrictive, + AnalyzerDiagnostics.ContextIsNotDeclared + ); - private const string PluginContextTypeName = "PluginInitContext"; + private const string PluginContextTypeName = "PluginInitContext"; - private const string PluginInterfaceName = "IPluginI18n"; + private const string PluginInterfaceName = "IPluginI18n"; - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); - } + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); + } - private static void AnalyzeNode(SyntaxNodeAnalysisContext context) - { - var classDeclaration = (ClassDeclarationSyntax)context.Node; - var semanticModel = context.SemanticModel; - var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + var semanticModel = context.SemanticModel; + var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); - if (!IsPluginEntryClass(classSymbol)) return; + if (!IsPluginEntryClass(classSymbol)) return; - var contextProperty = classDeclaration.Members.OfType() - .Select(p => semanticModel.GetDeclaredSymbol(p)) - .FirstOrDefault(p => p is { Type.Name: PluginContextTypeName }); + var contextProperty = classDeclaration.Members.OfType() + .Select(p => semanticModel.GetDeclaredSymbol(p)) + .FirstOrDefault(p => p?.Type.Name is PluginContextTypeName); - if (contextProperty is not null) - { - if (!contextProperty.IsStatic) + if (contextProperty != null) { - context.ReportDiagnostic(Diagnostic.Create( - AnalyzerDiagnostics.ContextIsNotStatic, - contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() - )); + if (!contextProperty.IsStatic) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextIsNotStatic, + contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + )); + return; + } + + if (contextProperty.DeclaredAccessibility is Accessibility.Private || contextProperty.DeclaredAccessibility is Accessibility.Protected) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextAccessIsTooRestrictive, + contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + )); + return; + } + return; } - if (contextProperty.DeclaredAccessibility is Accessibility.Private or Accessibility.Protected) + var fieldDeclaration = classDeclaration.Members + .OfType() + .SelectMany(f => f.Declaration.Variables) + .Select(f => semanticModel.GetDeclaredSymbol(f)) + .FirstOrDefault(f => f is IFieldSymbol fs && fs.Type.Name is PluginContextTypeName); + var parentSyntax = fieldDeclaration + ?.DeclaringSyntaxReferences[0] + .GetSyntax() + .FirstAncestorOrSelf(); + + if (parentSyntax != null) { context.ReportDiagnostic(Diagnostic.Create( - AnalyzerDiagnostics.ContextAccessIsTooRestrictive, - contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + AnalyzerDiagnostics.ContextIsAField, + parentSyntax.GetLocation() )); return; } - return; - } - - var fieldDeclaration = classDeclaration.Members - .OfType() - .SelectMany(f => f.Declaration.Variables) - .Select(f => semanticModel.GetDeclaredSymbol(f)) - .FirstOrDefault(f => f is IFieldSymbol { Type.Name: PluginContextTypeName }); - var parentSyntax = fieldDeclaration - ?.DeclaringSyntaxReferences[0] - .GetSyntax() - .FirstAncestorOrSelf(); - - if (parentSyntax is not null) - { context.ReportDiagnostic(Diagnostic.Create( - AnalyzerDiagnostics.ContextIsAField, - parentSyntax.GetLocation() + AnalyzerDiagnostics.ContextIsNotDeclared, + classDeclaration.Identifier.GetLocation() )); - return; } - context.ReportDiagnostic(Diagnostic.Create( - AnalyzerDiagnostics.ContextIsNotDeclared, - classDeclaration.Identifier.GetLocation() - )); + private static bool IsPluginEntryClass(INamedTypeSymbol namedTypeSymbol) => + namedTypeSymbol?.Interfaces.Any(i => i.Name == PluginInterfaceName) ?? false; } - - private static bool IsPluginEntryClass(INamedTypeSymbol? namedTypeSymbol) => - namedTypeSymbol?.Interfaces.Any(i => i.Name == PluginInterfaceName) ?? false; } diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs index f9a398cd7ad..fef040f40a4 100644 --- a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs +++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs @@ -1,5 +1,7 @@ using System.Collections.Immutable; using System.Composition; +using System.Linq; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -9,173 +11,173 @@ using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Text; -namespace Flow.Launcher.Analyzers.Localize; - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ContextAvailabilityAnalyzerCodeFixProvider)), Shared] -public class ContextAvailabilityAnalyzerCodeFixProvider : CodeFixProvider +namespace Flow.Launcher.Analyzers.Localize { - public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( - AnalyzerDiagnostics.ContextIsAField.Id, - AnalyzerDiagnostics.ContextIsNotStatic.Id, - AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id, - AnalyzerDiagnostics.ContextIsNotDeclared.Id - ); - - public sealed override FixAllProvider GetFixAllProvider() - { - return WellKnownFixAllProviders.BatchFixer; - } - - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ContextAvailabilityAnalyzerCodeFixProvider)), Shared] + public class ContextAvailabilityAnalyzerCodeFixProvider : CodeFixProvider { - var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - - var diagnostic = context.Diagnostics.First(); - var diagnosticSpan = diagnostic.Location.SourceSpan; + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( + AnalyzerDiagnostics.ContextIsAField.Id, + AnalyzerDiagnostics.ContextIsNotStatic.Id, + AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id, + AnalyzerDiagnostics.ContextIsNotDeclared.Id + ); - if (diagnostic.Id == AnalyzerDiagnostics.ContextIsAField.Id) + public sealed override FixAllProvider GetFixAllProvider() { - context.RegisterCodeFix( - CodeAction.Create( - title: "Replace with static property", - createChangedDocument: _ => Task.FromResult(FixContextIsAFieldError(context, root, diagnosticSpan)), - equivalenceKey: AnalyzerDiagnostics.ContextIsAField.Id - ), - diagnostic - ); + return WellKnownFixAllProviders.BatchFixer; } - else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotStatic.Id) + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { - context.RegisterCodeFix( - CodeAction.Create( - title: "Make static", - createChangedDocument: _ => Task.FromResult(FixContextIsNotStaticError(context, root, diagnosticSpan)), - equivalenceKey: AnalyzerDiagnostics.ContextIsNotStatic.Id - ), - diagnostic - ); + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + if (diagnostic.Id == AnalyzerDiagnostics.ContextIsAField.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with static property", + createChangedDocument: _ => Task.FromResult(FixContextIsAFieldError(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsAField.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotStatic.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Make static", + createChangedDocument: _ => Task.FromResult(FixContextIsNotStaticError(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsNotStatic.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Make internal", + createChangedDocument: _ => Task.FromResult(FixContextIsTooRestricted(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotDeclared.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Declare context property", + createChangedDocument: _ => Task.FromResult(FixContextNotDeclared(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsNotDeclared.Id + ), + diagnostic + ); + } } - else if (diagnostic.Id == AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id) - { - context.RegisterCodeFix( - CodeAction.Create( - title: "Make internal", - createChangedDocument: _ => Task.FromResult(FixContextIsTooRestricted(context, root, diagnosticSpan)), - equivalenceKey: AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id - ), - diagnostic + + private static MemberDeclarationSyntax GetStaticContextPropertyDeclaration(string propertyName = "Context") => + SyntaxFactory.ParseMemberDeclaration( + $"internal static PluginInitContext {propertyName} {{ get; private set; }} = null!;" ); - } - else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotDeclared.Id) + + private static Document GetFormattedDocument(CodeFixContext context, SyntaxNode root) { - context.RegisterCodeFix( - CodeAction.Create( - title: "Declare context property", - createChangedDocument: _ => Task.FromResult(FixContextNotDeclared(context, root, diagnosticSpan)), - equivalenceKey: AnalyzerDiagnostics.ContextIsNotDeclared.Id - ), - diagnostic + var formattedRoot = Formatter.Format( + root, + Formatter.Annotation, + context.Document.Project.Solution.Workspace ); - } - } - - private static MemberDeclarationSyntax? GetStaticContextPropertyDeclaration(string propertyName = "Context") => - SyntaxFactory.ParseMemberDeclaration( - $"internal static PluginInitContext {propertyName} {{ get; private set; }} = null!;" - ); - - private static Document GetFormattedDocument(CodeFixContext context, SyntaxNode root) - { - var formattedRoot = Formatter.Format( - root, - Formatter.Annotation, - context.Document.Project.Solution.Workspace - ); - return context.Document.WithSyntaxRoot(formattedRoot); - } - - private Document FixContextNotDeclared(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) - { - var classDeclaration = GetDeclarationSyntax(root, diagnosticSpan); - if (classDeclaration is not { BaseList: not null }) return context.Document; + return context.Document.WithSyntaxRoot(formattedRoot); + } - var newPropertyDeclaration = GetStaticContextPropertyDeclaration(); - if (newPropertyDeclaration is null) return context.Document; + private static Document FixContextNotDeclared(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) + { + var classDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (classDeclaration?.BaseList is null) return context.Document; - var annotatedNewPropertyDeclaration = newPropertyDeclaration - .WithLeadingTrivia(SyntaxFactory.ElasticLineFeed) - .WithTrailingTrivia(SyntaxFactory.ElasticLineFeed) - .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); + var newPropertyDeclaration = GetStaticContextPropertyDeclaration(); + if (newPropertyDeclaration is null) return context.Document; - var newMembers = classDeclaration.Members.Insert(0, annotatedNewPropertyDeclaration); - var newClassDeclaration = classDeclaration.WithMembers(newMembers); + var annotatedNewPropertyDeclaration = newPropertyDeclaration + .WithLeadingTrivia(SyntaxFactory.ElasticLineFeed) + .WithTrailingTrivia(SyntaxFactory.ElasticLineFeed) + .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); - var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration); + var newMembers = classDeclaration.Members.Insert(0, annotatedNewPropertyDeclaration); + var newClassDeclaration = classDeclaration.WithMembers(newMembers); - return GetFormattedDocument(context, newRoot); - } + var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration); - private static Document FixContextIsNotStaticError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) - { - var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); - if (propertyDeclaration is null) return context.Document; + return GetFormattedDocument(context, newRoot); + } - var newPropertyDeclaration = propertyDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + private static Document FixContextIsNotStaticError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) + { + var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (propertyDeclaration is null) return context.Document; - var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); - return context.Document.WithSyntaxRoot(newRoot); - } + var newPropertyDeclaration = propertyDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); - private static Document FixContextIsTooRestricted(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) - { - var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); - if (propertyDeclaration is null) return context.Document; + var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); + return context.Document.WithSyntaxRoot(newRoot); + } - var newModifiers = SyntaxFactory.TokenList(); - foreach (var modifier in propertyDeclaration.Modifiers) + private static Document FixContextIsTooRestricted(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) { - newModifiers = newModifiers.Add( - modifier.Kind() switch + var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (propertyDeclaration is null) return context.Document; + + var newModifiers = SyntaxFactory.TokenList(); + foreach (var modifier in propertyDeclaration.Modifiers) + { + if (modifier.IsKind(SyntaxKind.PrivateKeyword) || modifier.IsKind(SyntaxKind.ProtectedKeyword)) { - SyntaxKind.PrivateKeyword or SyntaxKind.ProtectedKeyword => SyntaxFactory.Token( - SyntaxKind.InternalKeyword - ), - _ => modifier + newModifiers = newModifiers.Add(SyntaxFactory.Token(SyntaxKind.InternalKeyword)); } - ); - } + else + { + newModifiers = newModifiers.Add(modifier); + } + } - var newPropertyDeclaration = propertyDeclaration.WithModifiers(newModifiers); + var newPropertyDeclaration = propertyDeclaration.WithModifiers(newModifiers); - var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); - return context.Document.WithSyntaxRoot(newRoot); - } + var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); + return context.Document.WithSyntaxRoot(newRoot); + } - private static Document FixContextIsAFieldError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) { - var fieldDeclaration = GetDeclarationSyntax(root, diagnosticSpan); - if (fieldDeclaration is null) return context.Document; + private static Document FixContextIsAFieldError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) { + var fieldDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (fieldDeclaration is null) return context.Document; - var field = fieldDeclaration.Declaration.Variables.First(); - var fieldIdentifier = field.Identifier.ToString(); + var field = fieldDeclaration.Declaration.Variables.First(); + var fieldIdentifier = field.Identifier.ToString(); - var propertyDeclaration = GetStaticContextPropertyDeclaration(fieldIdentifier); - if (propertyDeclaration is null) return context.Document; + var propertyDeclaration = GetStaticContextPropertyDeclaration(fieldIdentifier); + if (propertyDeclaration is null) return context.Document; - var annotatedNewPropertyDeclaration = propertyDeclaration - .WithTriviaFrom(fieldDeclaration) - .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); + var annotatedNewPropertyDeclaration = propertyDeclaration + .WithTriviaFrom(fieldDeclaration) + .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); - var newRoot = root.ReplaceNode(fieldDeclaration, annotatedNewPropertyDeclaration); + var newRoot = root.ReplaceNode(fieldDeclaration, annotatedNewPropertyDeclaration); - return GetFormattedDocument(context, newRoot); - } + return GetFormattedDocument(context, newRoot); + } - private static T? GetDeclarationSyntax(SyntaxNode root, TextSpan diagnosticSpan) where T : SyntaxNode => - root - .FindToken(diagnosticSpan.Start) - .Parent - ?.AncestorsAndSelf() - .OfType() - .First(); + private static T GetDeclarationSyntax(SyntaxNode root, TextSpan diagnosticSpan) where T : SyntaxNode => + root + .FindToken(diagnosticSpan.Start) + .Parent + ?.AncestorsAndSelf() + .OfType() + .First(); + } } diff --git a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs index 44f332330c9..25f0f6ef36b 100644 --- a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs +++ b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs @@ -1,96 +1,100 @@ using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace Flow.Launcher.Analyzers.Localize; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class OldGetTranslateAnalyzer : DiagnosticAnalyzer +namespace Flow.Launcher.Analyzers.Localize { - public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed); - - private static readonly string[] oldLocalizationClasses = { "IPublicAPI", "Internationalization" }; - private const string OldLocalizationMethodName = "GetTranslation"; - - private const string StringFormatMethodName = "Format"; - private const string StringFormatTypeName = "string"; - - public override void Initialize(AnalysisContext context) + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class OldGetTranslateAnalyzer : DiagnosticAnalyzer { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); - } + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed); - private static void AnalyzeNode(SyntaxNodeAnalysisContext context) - { - var invocationExpr = (InvocationExpressionSyntax)context.Node; - var semanticModel = context.SemanticModel; - var symbolInfo = semanticModel.GetSymbolInfo(invocationExpr); + private static readonly string[] oldLocalizationClasses = { "IPublicAPI", "Internationalization" }; + private const string OldLocalizationMethodName = "GetTranslation"; - if (symbolInfo.Symbol is not IMethodSymbol methodSymbol) return; + private const string StringFormatMethodName = "Format"; + private const string StringFormatTypeName = "string"; - if (IsFormatStringCall(methodSymbol) && - GetFirstArgumentInvocationExpression(invocationExpr) is {} innerInvocationExpr) + public override void Initialize(AnalysisContext context) { - if (!IsTranslateCall(semanticModel.GetSymbolInfo(innerInvocationExpr)) || - GetFirstArgumentStringValue(innerInvocationExpr) is not {} translationKey) - return; - - var diagnostic = Diagnostic.Create( - AnalyzerDiagnostics.OldLocalizationApiUsed, - invocationExpr.GetLocation(), - translationKey, - GetInvocationArguments(invocationExpr) - ); - context.ReportDiagnostic(diagnostic); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); } - else if (IsTranslateCall(methodSymbol) && GetFirstArgumentStringValue(invocationExpr) is {} translationKey) + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) { - if (IsParentFormatStringCall(semanticModel, invocationExpr)) return; - - var diagnostic = Diagnostic.Create( - AnalyzerDiagnostics.OldLocalizationApiUsed, - invocationExpr.GetLocation(), - translationKey, - string.Empty - ); - context.ReportDiagnostic(diagnostic); + var invocationExpr = (InvocationExpressionSyntax)context.Node; + var semanticModel = context.SemanticModel; + var symbolInfo = semanticModel.GetSymbolInfo(invocationExpr); + + if (!(symbolInfo.Symbol is IMethodSymbol methodSymbol)) return; + + if (IsFormatStringCall(methodSymbol) && + GetFirstArgumentInvocationExpression(invocationExpr) is InvocationExpressionSyntax innerInvocationExpr) + { + 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); + } + else if (IsTranslateCall(methodSymbol) && GetFirstArgumentStringValue(invocationExpr) is string translationKey) + { + if (IsParentFormatStringCall(semanticModel, invocationExpr)) return; + + var diagnostic = Diagnostic.Create( + AnalyzerDiagnostics.OldLocalizationApiUsed, + invocationExpr.GetLocation(), + translationKey, + string.Empty + ); + context.ReportDiagnostic(diagnostic); + } } - } - private static string GetInvocationArguments(InvocationExpressionSyntax invocationExpr) => - string.Join(", ", invocationExpr.ArgumentList.Arguments.Skip(1)); + private static string GetInvocationArguments(InvocationExpressionSyntax invocationExpr) => + string.Join(", ", invocationExpr.ArgumentList.Arguments.Skip(1)); - private static bool IsParentFormatStringCall(SemanticModel? semanticModel, SyntaxNode? syntaxNode) => - syntaxNode is InvocationExpressionSyntax { Parent.Parent.Parent: {} parent } && - IsFormatStringCall(semanticModel?.GetSymbolInfo(parent)); + 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 bool IsFormatStringCall(SymbolInfo? symbolInfo) => - symbolInfo is { Symbol: IMethodSymbol methodSymbol } && IsFormatStringCall(methodSymbol); + private static bool IsFormatStringCall(SymbolInfo? symbolInfo) => + symbolInfo is SymbolInfo info && IsFormatStringCall(info.Symbol as IMethodSymbol); - private static bool IsFormatStringCall(IMethodSymbol? methodSymbol) => - methodSymbol is { Name: StringFormatMethodName } && - methodSymbol.ContainingType.ToDisplayString() is StringFormatTypeName; + private static bool IsFormatStringCall(IMethodSymbol methodSymbol) => + methodSymbol?.Name is StringFormatMethodName && + methodSymbol.ContainingType.ToDisplayString() is StringFormatTypeName; - private static InvocationExpressionSyntax? GetFirstArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr) => - invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression as InvocationExpressionSyntax; + private static InvocationExpressionSyntax GetFirstArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr) => + invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression as InvocationExpressionSyntax; - private static bool IsTranslateCall(SymbolInfo symbolInfo) => - symbolInfo.Symbol is IMethodSymbol { Name: OldLocalizationMethodName } innerMethodSymbol && - oldLocalizationClasses.Contains(innerMethodSymbol.ContainingType.Name); + private static bool IsTranslateCall(SymbolInfo symbolInfo) => + symbolInfo.Symbol is IMethodSymbol innerMethodSymbol && + innerMethodSymbol.Name is OldLocalizationMethodName && + oldLocalizationClasses.Contains(innerMethodSymbol.ContainingType.Name); - private static bool IsTranslateCall(IMethodSymbol methodSymbol) => - methodSymbol is { Name: OldLocalizationMethodName } && - oldLocalizationClasses.Contains(methodSymbol.ContainingType.Name); + private static bool IsTranslateCall(IMethodSymbol methodSymbol) => + methodSymbol?.Name is OldLocalizationMethodName && + oldLocalizationClasses.Contains(methodSymbol.ContainingType.Name); - private static string? GetFirstArgumentStringValue(InvocationExpressionSyntax invocationExpr) => - invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression switch + private static string GetFirstArgumentStringValue(InvocationExpressionSyntax invocationExpr) { - LiteralExpressionSyntax syntax => syntax.Token.ValueText, - _ => null - }; + if (invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression is LiteralExpressionSyntax syntax) + return syntax.Token.ValueText; + return null; + } + } } diff --git a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs index 1742335883f..87fb37a8355 100644 --- a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs +++ b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs @@ -1,102 +1,114 @@ using System.Collections.Immutable; using System.Composition; +using System.Linq; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Flow.Launcher.Analyzers.Localize; - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OldGetTranslateAnalyzerCodeFixProvider)), Shared] -public class OldGetTranslateAnalyzerCodeFixProvider : CodeFixProvider +namespace Flow.Launcher.Analyzers.Localize { - public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed.Id); - - public sealed override FixAllProvider GetFixAllProvider() + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OldGetTranslateAnalyzerCodeFixProvider)), Shared] + public class OldGetTranslateAnalyzerCodeFixProvider : CodeFixProvider { - return WellKnownFixAllProviders.BatchFixer; - } + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed.Id); - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - - var diagnostic = context.Diagnostics.First(); - - context.RegisterCodeFix( - CodeAction.Create( - title: "Replace with 'Localize.localization_key(...args)'", - createChangedDocument: _ => Task.FromResult(FixOldTranslation(context, root, diagnostic)), - equivalenceKey: AnalyzerDiagnostics.OldLocalizationApiUsed.Id - ), - diagnostic - ); - } + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } - private static Document FixOldTranslation(CodeFixContext context, SyntaxNode? root, Diagnostic diagnostic) - { - var diagnosticSpan = diagnostic.Location.SourceSpan; + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var invocationExpr = root - ?.FindToken(diagnosticSpan.Start).Parent - ?.AncestorsAndSelf() - .OfType() - .First(); + var diagnostic = context.Diagnostics.First(); - if (invocationExpr is null || root is null) return context.Document; + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with 'Localize.localization_key(...args)'", + createChangedDocument: _ => Task.FromResult(FixOldTranslation(context, root, diagnostic)), + equivalenceKey: AnalyzerDiagnostics.OldLocalizationApiUsed.Id + ), + diagnostic + ); + } - var argumentList = invocationExpr.ArgumentList.Arguments; - var argument = argumentList.First().Expression; + private static Document FixOldTranslation(CodeFixContext context, SyntaxNode root, Diagnostic diagnostic) + { + var diagnosticSpan = diagnostic.Location.SourceSpan; - if (GetTranslationKey(argument) is { } translationKey) - return FixOldTranslationWithoutStringFormat(context, translationKey, root, invocationExpr); + var invocationExpr = root + ?.FindToken(diagnosticSpan.Start).Parent + ?.AncestorsAndSelf() + .OfType() + .First(); - if (GetTranslationKeyFromInnerInvocation(argument) is { } translationKeyInside) - return FixOldTranslationWithStringFormat(context, argumentList, translationKeyInside, root, invocationExpr); + if (invocationExpr is null || root is null) return context.Document; - 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); - private static string? GetTranslationKey(ExpressionSyntax syntax) => - syntax switch - { - LiteralExpressionSyntax { Token.Value: string translationKey } => translationKey, - _ => null - }; - private static Document FixOldTranslationWithoutStringFormat( - CodeFixContext context, string translationKey, SyntaxNode root, InvocationExpressionSyntax invocationExpr - ) { - var newInvocationExpr = SyntaxFactory.ParseExpression( - $"Localize.{translationKey}()" - ); - - var newRoot = root.ReplaceNode(invocationExpr, newInvocationExpr); - var newDocument = context.Document.WithSyntaxRoot(newRoot); - return newDocument; - } + if (GetTranslationKeyFromInnerInvocation(argument) is string translationKeyInside) + return FixOldTranslationWithStringFormat(context, argumentList, translationKeyInside, root, invocationExpr); + + return context.Document; + } - private static string? GetTranslationKeyFromInnerInvocation(ExpressionSyntax syntax) => - syntax switch + + private static string GetTranslationKey(ExpressionSyntax syntax) { - InvocationExpressionSyntax { ArgumentList.Arguments: { Count: 1 } arguments } => - GetTranslationKey(arguments.First().Expression), - _ => null - }; - - private static Document FixOldTranslationWithStringFormat( - CodeFixContext context, - SeparatedSyntaxList argumentList, - string translationKey2, - SyntaxNode root, - InvocationExpressionSyntax invocationExpr - ) { - var newArguments = string.Join(", ", argumentList.Skip(1).Select(a => a.Expression)); - var newInnerInvocationExpr = SyntaxFactory.ParseExpression($"Localize.{translationKey2}({newArguments})"); - - var newRoot = root.ReplaceNode(invocationExpr, newInnerInvocationExpr); - return context.Document.WithSyntaxRoot(newRoot); - } + 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 + ) { + var newInvocationExpr = SyntaxFactory.ParseExpression( + $"Localize.{translationKey}()" + ); + + var newRoot = root.ReplaceNode(invocationExpr, newInvocationExpr); + var newDocument = context.Document.WithSyntaxRoot(newRoot); + return newDocument; + } + + private static string GetTranslationKeyFromInnerInvocation(ExpressionSyntax syntax) + { + if ( + syntax is InvocationExpressionSyntax invocationExpressionSyntax && + invocationExpressionSyntax.ArgumentList.Arguments.Count is 1 + ) + { + var firstArgument = invocationExpressionSyntax.ArgumentList.Arguments.First().Expression; + return GetTranslationKey(firstArgument); + } + return null; + } + + private static Document FixOldTranslationWithStringFormat( + CodeFixContext context, + SeparatedSyntaxList argumentList, + string translationKey2, + SyntaxNode root, + InvocationExpressionSyntax invocationExpr + ) { + var newArguments = string.Join(", ", argumentList.Skip(1).Select(a => a.Expression)); + var newInnerInvocationExpr = SyntaxFactory.ParseExpression($"Localize.{translationKey2}({newArguments})"); + + var newRoot = root.ReplaceNode(invocationExpr, newInnerInvocationExpr); + return context.Document.WithSyntaxRoot(newRoot); + } + } } diff --git a/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj b/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj index e192c04dcc7..9679baa17e0 100644 --- a/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj +++ b/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj @@ -2,9 +2,7 @@ 1.0.0 - net7.0 - enable - enable + netstandard2.0 true Flow.Launcher.SourceGenerators diff --git a/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs index 9c984d5b9b7..ca8d6d5a01d 100644 --- a/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs +++ b/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Collections.Generic; +using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Xml.Linq; using Microsoft.CodeAnalysis; @@ -6,78 +8,96 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -namespace Flow.Launcher.SourceGenerators.Localize; - -[Generator] -public partial class LocalizeSourceGenerator : ISourceGenerator +namespace Flow.Launcher.SourceGenerators.Localize { - private OptimizationLevel _optimizationLevel; - - private const string CoreNamespace1 = "Flow.Launcher"; - private const string CoreNamespace2 = "Flow.Launcher.Core"; - private const string DefaultNamespace = "Flow.Launcher"; - private const string ClassName = "Localize"; - private const string PluginInterfaceName = "IPluginI18n"; - private const string PluginContextTypeName = "PluginInitContext"; - private const string KeywordStatic = "static"; - private const string KeywordPrivate = "private"; - private const string KeywordProtected = "protected"; - private const string XamlPrefix = "system"; - private const string XamlTag = "String"; - - private const string DefaultLanguageFilePathEndsWith = @"\Languages\en.xaml"; - private const string XamlCustomPathPropertyKey = "build_property.localizegeneratorlangfiles"; - private readonly char[] _xamlCustomPathPropertyDelimiters = { '\n', ';' }; - private readonly Regex _languagesXamlRegex = RegexEndsWithLanguagesXaml(); - - public void Initialize(GeneratorInitializationContext context) - { - } - - public void Execute(GeneratorExecutionContext context) + [Generator] + public partial class LocalizeSourceGenerator : ISourceGenerator { - _optimizationLevel = context.Compilation.Options.OptimizationLevel; - - context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( - XamlCustomPathPropertyKey, - out var langFilePathEndsWithStr - ); - - var allLanguageKeys = new HashSet(); - context.Compilation.SyntaxTrees - .SelectMany(v => v.GetRoot().DescendantNodes().OfType()) - .ToList() - .ForEach( - v => - { - var split = v.Expression.ToString().Split('.'); - if (split.Length < 2) return; - if (split[0] is not ClassName) return; - allLanguageKeys.Add(split[1]); - }); - - var allXamlFiles = context.AdditionalFiles - .Where(v => _languagesXamlRegex.IsMatch(v.Path)) - .ToArray(); - AdditionalText[] resourceDictionaries; - if (allXamlFiles.Length is 0) + private OptimizationLevel _optimizationLevel; + + private const string CoreNamespace1 = "Flow.Launcher"; + private const string CoreNamespace2 = "Flow.Launcher.Core"; + private const string DefaultNamespace = "Flow.Launcher"; + private const string ClassName = "Localize"; + private const string PluginInterfaceName = "IPluginI18n"; + private const string PluginContextTypeName = "PluginInitContext"; + private const string KeywordStatic = "static"; + private const string KeywordPrivate = "private"; + private const string KeywordProtected = "protected"; + private const string XamlPrefix = "system"; + private const string XamlTag = "String"; + + private const string DefaultLanguageFilePathEndsWith = @"\Languages\en.xaml"; + private const string XamlCustomPathPropertyKey = "build_property.localizegeneratorlangfiles"; + private readonly char[] _xamlCustomPathPropertyDelimiters = { '\n', ';' }; + private readonly Regex _languagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase); + + public void Initialize(GeneratorInitializationContext context) { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, - Location.None - )); - return; } - if (string.IsNullOrEmpty(langFilePathEndsWithStr)) + public void Execute(GeneratorExecutionContext context) { - if (allXamlFiles.Length is 1) + _optimizationLevel = context.Compilation.Options.OptimizationLevel; + + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( + XamlCustomPathPropertyKey, + out var langFilePathEndsWithStr + ); + + var allLanguageKeys = new List(); + context.Compilation.SyntaxTrees + .SelectMany(v => v.GetRoot().DescendantNodes().OfType()) + .ToList() + .ForEach( + v => + { + var split = v.Expression.ToString().Split('.'); + if (split.Length < 2) return; + if (!(split[0] is ClassName)) return; + allLanguageKeys.Add(split[1]); + }); + + var allXamlFiles = context.AdditionalFiles + .Where(v => _languagesXamlRegex.IsMatch(v.Path)) + .ToArray(); + AdditionalText[] resourceDictionaries; + if (allXamlFiles.Length is 0) { - resourceDictionaries = allXamlFiles; + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, + Location.None + )); + return; + } + + if (string.IsNullOrEmpty(langFilePathEndsWithStr)) + { + if (allXamlFiles.Length is 1) + { + resourceDictionaries = allXamlFiles; + } + else + { + resourceDictionaries = allXamlFiles.Where(v => v.Path.EndsWith(DefaultLanguageFilePathEndsWith)).ToArray(); + if (resourceDictionaries.Length is 0) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, + Location.None + )); + return; + } + } } else { - resourceDictionaries = allXamlFiles.Where(v => v.Path.EndsWith(DefaultLanguageFilePathEndsWith)).ToArray(); + var langFilePathEndings = langFilePathEndsWithStr + .Trim() + .Split(_xamlCustomPathPropertyDelimiters) + .Select(v => v.Trim()) + .ToArray(); + resourceDictionaries = allXamlFiles.Where(v => langFilePathEndings.Any(v.Path.EndsWith)).ToArray(); if (resourceDictionaries.Length is 0) { context.ReportDiagnostic(Diagnostic.Create( @@ -87,360 +107,385 @@ out var langFilePathEndsWithStr return; } } - } - else - { - var langFilePathEndings = langFilePathEndsWithStr - .Trim() - .Split(_xamlCustomPathPropertyDelimiters) - .Select(v => v.Trim()) - .ToArray(); - resourceDictionaries = allXamlFiles.Where(v => langFilePathEndings.Any(v.Path.EndsWith)).ToArray(); - if (resourceDictionaries.Length is 0) - { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, - Location.None - )); - return; - } - } - var ns = context.Compilation.AssemblyName ?? DefaultNamespace; + var ns = context.Compilation.AssemblyName ?? DefaultNamespace; - var localizedStrings = LoadLocalizedStrings(resourceDictionaries); + var localizedStrings = LoadLocalizedStrings(resourceDictionaries); - var unusedLocalizationKeys = localizedStrings.Keys.Except(allLanguageKeys).ToHashSet(); + var unusedLocalizationKeys = localizedStrings.Keys.Except(allLanguageKeys).ToArray(); - foreach (var key in unusedLocalizationKeys) - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.LocalizationKeyUnused, - Location.None, - key - )); - - var sourceCode = GenerateSourceCode(localizedStrings, context, unusedLocalizationKeys); + foreach (var key in unusedLocalizationKeys) + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.LocalizationKeyUnused, + Location.None, + key + )); - context.AddSource($"{ClassName}.{ns}.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); - } + var sourceCode = GenerateSourceCode(localizedStrings, context, unusedLocalizationKeys); - private static Dictionary LoadLocalizedStrings(IEnumerable files) - { - var result = new Dictionary(); + context.AddSource($"{ClassName}.{ns}.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); + } - foreach (var file in files) + private static Dictionary LoadLocalizedStrings(AdditionalText[] files) { - ProcessXamlFile(file, result); - } + var result = new Dictionary(); - return result; - } + foreach (var file in files) + { + ProcessXamlFile(file, result); + } - private static void ProcessXamlFile(AdditionalText file, Dictionary result) { - var content = file.GetText()?.ToString(); - if (content is null) return; - var doc = XDocument.Parse(content); - var ns = doc.Root?.GetNamespaceOfPrefix(XamlPrefix); - if (ns is null) return; - foreach (var element in doc.Descendants(ns + XamlTag)) - { - var name = element.FirstAttribute?.Value; - var value = element.Value; + return result; + } - if (name is null) continue; + private static void ProcessXamlFile(AdditionalText file, Dictionary result) { + var content = file.GetText()?.ToString(); + if (content is null) return; + var doc = XDocument.Parse(content); + var ns = doc.Root?.GetNamespaceOfPrefix(XamlPrefix); + if (ns is null) return; + foreach (var element in doc.Descendants(ns + XamlTag)) + { + var name = element.FirstAttribute?.Value; + var value = element.Value; + + if (name is null) continue; - string? summary = null; - var paramsList = new List(); - var commentNode = element.PreviousNode; + string summary = null; + var paramsList = new List(); + var commentNode = element.PreviousNode; - if (commentNode is XComment comment) - summary = ProcessXamlFileComment(comment, paramsList); + if (commentNode is XComment comment) + summary = ProcessXamlFileComment(comment, paramsList); - result[name] = new LocalizableString(name, value, summary, paramsList); + result[name] = new LocalizableString(name, value, summary, paramsList); + } } - } - private static string? ProcessXamlFileComment(XComment comment, List paramsList) { - string? summary = null; - try - { - if (CommentIncludesDocumentationMarkup(comment)) + private static string ProcessXamlFileComment(XComment comment, List paramsList) { + string summary = null; + try { - var commentDoc = XDocument.Parse($"{comment.Value}"); - summary = ExtractDocumentationCommentSummary(commentDoc); - foreach (var param in commentDoc.Descendants("param")) + if (CommentIncludesDocumentationMarkup(comment)) { - var index = int.Parse(param.Attribute("index")?.Value ?? "-1"); - var paramName = param.Attribute("name")?.Value; - var paramType = param.Attribute("type")?.Value; - if (index < 0 || paramName is null || paramType is null) continue; - paramsList.Add(new LocalizableStringParam(index, paramName, paramType)); + var commentDoc = XDocument.Parse($"{comment.Value}"); + summary = ExtractDocumentationCommentSummary(commentDoc); + foreach (var param in commentDoc.Descendants("param")) + { + var index = int.Parse(param.Attribute("index")?.Value ?? "-1"); + var paramName = param.Attribute("name")?.Value; + var paramType = param.Attribute("type")?.Value; + if (index < 0 || paramName is null || paramType is null) continue; + paramsList.Add(new LocalizableStringParam(index, paramName, paramType)); + } } } + catch + { + // ignore + } + + return summary; } - catch - { - // ignore + + private static string ExtractDocumentationCommentSummary(XDocument commentDoc) { + return commentDoc.Descendants("summary").FirstOrDefault()?.Value.Trim(); } - return summary; - } + private static bool CommentIncludesDocumentationMarkup(XComment comment) { + return comment.Value.Contains("") || comment.Value.Contains(" localizedStrings, + GeneratorExecutionContext context, + string[] unusedLocalizationKeys + ) + { + var ns = context.Compilation.AssemblyName; - private static bool CommentIncludesDocumentationMarkup(XComment comment) { - return comment.Value.Contains("", StringComparison.OrdinalIgnoreCase) || - comment.Value.Contains(" localizedStrings, - GeneratorExecutionContext context, - HashSet unusedLocalizationKeys - ) - { - var ns = context.Compilation.AssemblyName; + string contextPropertyName = null; + var mainClassFound = false; + foreach (var (syntaxTree, classDeclaration) in GetClasses(context)) + { + if (!DoesClassImplementInterface(classDeclaration, PluginInterfaceName)) + continue; - var sb = new StringBuilder(); - if (ns is CoreNamespace1 or CoreNamespace2) - { - GenerateFileHeader(sb, context); - GenerateClass(sb, localizedStrings, unusedLocalizationKeys); - return sb.ToString(); - } + mainClassFound = true; - string? contextPropertyName = null; - var mainClassFound = false; - foreach (var (syntaxTree, classDeclaration) in GetClasses(context)) - { - if (!DoesClassImplementInterface(classDeclaration, PluginInterfaceName)) - continue; + var property = GetPluginContextProperty(classDeclaration); + if (property is null) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindContextProperty, + GetLocation(syntaxTree, classDeclaration), + classDeclaration.Identifier + )); + return string.Empty; + } - mainClassFound = true; + var propertyModifiers = GetPropertyModifiers(property); - var property = GetPluginContextProperty(classDeclaration); - if (property is null) - { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.CouldNotFindContextProperty, - GetLocation(syntaxTree, classDeclaration), - classDeclaration.Identifier - )); - return string.Empty; - } + if (!propertyModifiers.Static) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyNotStatic, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } - var propertyModifiers = GetPropertyModifiers(property); + if (propertyModifiers.Private) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyIsPrivate, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } - if (!propertyModifiers.Static) - { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.ContextPropertyNotStatic, - GetLocation(syntaxTree, property), - property.Identifier - )); - return string.Empty; - } + if (propertyModifiers.Protected) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyIsProtected, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } - if (propertyModifiers.Private) - { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.ContextPropertyIsPrivate, - GetLocation(syntaxTree, property), - property.Identifier - )); - return string.Empty; + contextPropertyName = $"{classDeclaration.Identifier}.{property.Identifier}"; + break; } - if (propertyModifiers.Protected) + if (mainClassFound is false) { context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.ContextPropertyIsProtected, - GetLocation(syntaxTree, property), - property.Identifier + SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass, + Location.None )); return string.Empty; } - contextPropertyName = $"{classDeclaration.Identifier}.{property.Identifier}"; - break; + GenerateFileHeader(sb, context, true); + GenerateClass(sb, localizedStrings, unusedLocalizationKeys, contextPropertyName); + return sb.ToString(); } - if (mainClassFound is false) + private static void GenerateFileHeader(StringBuilder sb, GeneratorExecutionContext context, bool isPlugin = false) { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass, - Location.None - )); - return string.Empty; - } - - GenerateFileHeader(sb, context, true); - GenerateClass(sb, localizedStrings, unusedLocalizationKeys, contextPropertyName); - return sb.ToString(); - } - - private static void GenerateFileHeader(StringBuilder sb, GeneratorExecutionContext context, bool isPlugin = false) - { - var rootNamespace = context.Compilation.AssemblyName; - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); + var rootNamespace = context.Compilation.AssemblyName; + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); - if (!isPlugin) - sb.AppendLine("using Flow.Launcher.Core.Resource;"); + if (!isPlugin) + sb.AppendLine("using Flow.Launcher.Core.Resource;"); - sb.AppendLine($"namespace {rootNamespace};"); - } + sb.AppendLine($"namespace {rootNamespace};"); + } - private void GenerateClass( - StringBuilder sb, - Dictionary localizedStrings, - HashSet unusedLocalizationKeys, - string? propertyName = null - ) - { - sb.AppendLine(); - sb.AppendLine($"public static class {ClassName}"); - sb.AppendLine("{"); - foreach (var localizedString in localizedStrings) + private void GenerateClass( + StringBuilder sb, + Dictionary localizedStrings, + string[] unusedLocalizationKeys, + string propertyName = null + ) { - if (_optimizationLevel == OptimizationLevel.Release && unusedLocalizationKeys.Contains(localizedString.Key)) - continue; - - GenerateDocCommentForMethod(sb, localizedString.Value); - GenerateMethod(sb, localizedString.Value, propertyName); - } + sb.AppendLine(); + sb.AppendLine($"public static class {ClassName}"); + sb.AppendLine("{"); + foreach (var localizedString in localizedStrings) + { + if (_optimizationLevel == OptimizationLevel.Release && unusedLocalizationKeys.Contains(localizedString.Key)) + continue; - sb.AppendLine("}"); - } + GenerateDocCommentForMethod(sb, localizedString.Value); + GenerateMethod(sb, localizedString.Value, propertyName); + } - private static void GenerateDocCommentForMethod(StringBuilder sb, LocalizableString localizableString) - { - sb.AppendLine("/// "); - if (localizableString.Summary is not null) - { - sb.AppendLine(string.Join('\n', localizableString.Summary.Trim().Split("\n").Select(v => $"/// {v}"))); + sb.AppendLine("}"); } - sb.AppendLine("/// "); - var value = localizableString.Value; - foreach (var p in localizableString.Params) + private static void GenerateDocCommentForMethod(StringBuilder sb, LocalizableString localizableString) { - value = value.Replace($"{{{p.Index}}}", $"{{{p.Name}}}"); + sb.AppendLine("/// "); + if (!(localizableString.Summary is null)) + { + sb.AppendLine(string.Join("\n", localizableString.Summary.Trim().Split('\n').Select(v => $"/// {v}"))); + } + + sb.AppendLine("/// "); + var value = localizableString.Value; + foreach (var p in localizableString.Params) + { + value = value.Replace($"{{{p.Index}}}", $"{{{p.Name}}}"); + } + sb.AppendLine(string.Join("\n", value.Split('\n').Select(v => $"/// {v}"))); + sb.AppendLine("/// "); + sb.AppendLine("/// "); } - sb.AppendLine(string.Join('\n', value.Split("\n").Select(v => $"/// {v}"))); - sb.AppendLine("/// "); - sb.AppendLine("/// "); - } - private static void GenerateMethod(StringBuilder sb, LocalizableString localizableString, string? contextPropertyName) - { - sb.Append($"public static string {localizableString.Key}("); - var declarationArgs = new List(); - var callArgs = new List(); - for (var i = 0; i < 10; i++) + private static void GenerateMethod(StringBuilder sb, LocalizableString localizableString, string contextPropertyName) { - if (localizableString.Value.Contains($"{{{i}}}")) + sb.Append($"public static string {localizableString.Key}("); + var declarationArgs = new List(); + var callArgs = new List(); + for (var i = 0; i < 10; i++) { - var param = localizableString.Params.FirstOrDefault(v => v.Index == i); - if (param is not null) + if (localizableString.Value.Contains($"{{{i}}}")) { - declarationArgs.Add($"{param.Type} {param.Name}"); - callArgs.Add(param.Name); + var param = localizableString.Params.FirstOrDefault(v => v.Index == i); + if (!(param is null)) + { + declarationArgs.Add($"{param.Type} {param.Name}"); + callArgs.Add(param.Name); + } + else + { + declarationArgs.Add($"object? arg{i}"); + callArgs.Add($"arg{i}"); + } } else { - declarationArgs.Add($"object? arg{i}"); - callArgs.Add($"arg{i}"); + break; } } - else + + string callArray; + switch (callArgs.Count) { - break; + case 0: + callArray = ""; + break; + case 1: + callArray = callArgs[0]; + break; + default: + callArray = $"new object?[] {{ {string.Join(", ", callArgs)} }}"; + break; } - } - var callArray = callArgs.Count switch - { - 0 => "", - 1 => callArgs[0], - _ => $"new object?[] {{ {string.Join(", ", callArgs)} }}" - }; - - sb.Append(string.Join(", ", declarationArgs)); - sb.Append(") => "); - if (contextPropertyName is null) - { - if (string.IsNullOrEmpty(callArray)) + sb.Append(string.Join(", ", declarationArgs)); + sb.Append(") => "); + if (contextPropertyName is null) { - sb.AppendLine($"""InternationalizationManager.Instance.GetTranslation("{localizableString.Key}");"""); + if (string.IsNullOrEmpty(callArray)) + { + sb.AppendLine($"InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\");"); + } + else + { + sb.AppendLine( + $"string.Format(InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\"), {callArray});" + ); + } } else { - sb.AppendLine( - $"""string.Format(InternationalizationManager.Instance.GetTranslation("{localizableString.Key}"), {callArray});""" - ); + if (string.IsNullOrEmpty(callArray)) + { + sb.AppendLine($"{contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\");"); + } + else + { + sb.AppendLine($"string.Format({contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\"), {callArray});"); + } } + + sb.AppendLine(); } - else + + private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration) { - if (string.IsNullOrEmpty(callArray)) - { - sb.AppendLine($"""{contextPropertyName}.API.GetTranslation("{localizableString.Key}");"""); - } - else + return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan); + } + + private static IEnumerable<(SyntaxTree, ClassDeclarationSyntax)> GetClasses(GeneratorExecutionContext context) + { + foreach (var syntaxTree in context.Compilation.SyntaxTrees) { - sb.AppendLine($"""string.Format({contextPropertyName}.API.GetTranslation("{localizableString.Key}"), {callArray});"""); + var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType(); + foreach (var classDeclaration in classDeclarations) + { + yield return (syntaxTree, classDeclaration); + } } } - sb.AppendLine(); - } + private static bool DoesClassImplementInterface(ClassDeclarationSyntax classDeclaration, string interfaceName) + { + return classDeclaration.BaseList?.Types.Any(v => interfaceName == v.ToString()) is true; + } - private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration) - { - return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan); - } + private static PropertyDeclarationSyntax GetPluginContextProperty(ClassDeclarationSyntax classDeclaration) + { + return classDeclaration.Members + .OfType() + .FirstOrDefault(v => v.Type.ToString() is PluginContextTypeName); + } - private static IEnumerable<(SyntaxTree, ClassDeclarationSyntax)> GetClasses(GeneratorExecutionContext context) - { - foreach (var syntaxTree in context.Compilation.SyntaxTrees) + private static Modifiers GetPropertyModifiers(PropertyDeclarationSyntax property) + { + var isStatic = property.Modifiers.Any(v => v.Text is KeywordStatic); + var isPrivate = property.Modifiers.Any(v => v.Text is KeywordPrivate); + var isProtected = property.Modifiers.Any(v => v.Text is KeywordProtected); + + return new Modifiers(isStatic, isPrivate, isProtected); + } + + private class Modifiers { - var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType(); - foreach (var classDeclaration in classDeclarations) + public bool Static { get; } + public bool Private { get; } + public bool Protected { get; } + + public Modifiers(bool isStatic = false, bool isPrivate = false, bool isProtected = false) { - yield return (syntaxTree, classDeclaration); + Static = isStatic; + Private = isPrivate; + Protected = isProtected; } } } - private static bool DoesClassImplementInterface(ClassDeclarationSyntax classDeclaration, string interfaceName) + public class LocalizableStringParam { - return classDeclaration.BaseList?.Types.Any(v => interfaceName == v.ToString()) is true; - } + public int Index { get; } + public string Name { get; } + public string Type { get; } - private static PropertyDeclarationSyntax? GetPluginContextProperty(ClassDeclarationSyntax classDeclaration) - { - return classDeclaration.Members - .OfType() - .FirstOrDefault(v => v.Type.ToString() is PluginContextTypeName); + public LocalizableStringParam(int index, string name, string type) + { + Index = index; + Name = name; + Type = type; + } } - private static Modifiers GetPropertyModifiers(PropertyDeclarationSyntax property) + public class LocalizableString { - var isStatic = property.Modifiers.Any(v => v.Text is KeywordStatic); - var isPrivate = property.Modifiers.Any(v => v.Text is KeywordPrivate); - var isProtected = property.Modifiers.Any(v => v.Text is KeywordProtected); + public string Key { get; } + public string Value { get; } + public string Summary { get; } + public IEnumerable Params { get; } - return new Modifiers(isStatic, isPrivate, isProtected); + public LocalizableString(string key, string value, string summary, IEnumerable @params) + { + Key = key; + Value = value; + Summary = summary; + Params = @params; + } } - - private record Modifiers(bool Static = false, bool Private = false, bool Protected = false); - - [GeneratedRegex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase)] - private static partial Regex RegexEndsWithLanguagesXaml(); } - -public record LocalizableString(string Key, string Value, string? Summary, IEnumerable Params); -public record LocalizableStringParam(int Index, string Name, string Type); diff --git a/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs b/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs index ba7992dbca4..296419bd3e1 100644 --- a/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs +++ b/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs @@ -1,69 +1,70 @@ using Microsoft.CodeAnalysis; -namespace Flow.Launcher.SourceGenerators; - -public static class SourceGeneratorDiagnostics +namespace Flow.Launcher.SourceGenerators { - public static readonly DiagnosticDescriptor CouldNotFindResourceDictionaries = new( - "FLSG0001", - "Could not find resource dictionaries", - "Could not find resource dictionaries. There must be a file named [LANG].xaml file (for example, en.xaml), and it must be specified in in your .csproj file.", - "Localization", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static class SourceGeneratorDiagnostics + { + public static readonly DiagnosticDescriptor CouldNotFindResourceDictionaries = new DiagnosticDescriptor( + "FLSG0001", + "Could not find resource dictionaries", + "Could not find resource dictionaries. There must be a file named [LANG].xaml file (for example, en.xaml), and it must be specified in in your .csproj file.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor CouldNotFindPluginEntryClass = new( - "FLSG0002", - "Could not find the main class of plugin", - "Could not find the main class of your plugin. It must implement IPluginI18n.", - "Localization", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor CouldNotFindPluginEntryClass = new DiagnosticDescriptor( + "FLSG0002", + "Could not find the main class of plugin", + "Could not find the main class of your plugin. It must implement IPluginI18n.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor CouldNotFindContextProperty = new( - "FLSG0003", - "Could not find plugin context property", - "Could not find a property of type PluginInitContext in {0}. It must be a public static or internal static property of the main class of your plugin.", - "Localization", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor CouldNotFindContextProperty = new DiagnosticDescriptor( + "FLSG0003", + "Could not find plugin context property", + "Could not find a property of type PluginInitContext in {0}. It must be a public static or internal static property of the main class of your plugin.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor ContextPropertyNotStatic = new( - "FLSG0004", - "Plugin context property is not static", - "Context property {0} is not static. It must be static.", - "Localization", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor ContextPropertyNotStatic = new DiagnosticDescriptor( + "FLSG0004", + "Plugin context property is not static", + "Context property {0} is not static. It must be static.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor ContextPropertyIsPrivate = new( - "FLSG0005", - "Plugin context property is private", - "Context property {0} is private. It must be either internal or public.", - "Localization", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor ContextPropertyIsPrivate = new DiagnosticDescriptor( + "FLSG0005", + "Plugin context property is private", + "Context property {0} is private. It must be either internal or public.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor ContextPropertyIsProtected = new( - "FLSG0006", - "Plugin context property is protected", - "Context property {0} is protected. It must be either internal or public.", - "Localization", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor ContextPropertyIsProtected = new DiagnosticDescriptor( + "FLSG0006", + "Plugin context property is protected", + "Context property {0} is protected. It must be either internal or public.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor LocalizationKeyUnused = new( - "FLSG0007", - "Localization key is unused", - "Method 'Localize.{0}' is never used", - "Localization", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor LocalizationKeyUnused = new DiagnosticDescriptor( + "FLSG0007", + "Localization key is unused", + "Method 'Localize.{0}' is never used", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + } } From ba18f757d590ef7dfc15303951a8c3fd7ca08f5c Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sat, 1 Jun 2024 06:23:31 +0600 Subject: [PATCH 4/4] Code fix provider for localization: when fixing "property is not static" error, also fix it being too restrictive --- .../AnalyzerDiagnostics.cs | 8 ++--- ...textAvailabilityAnalyzerCodeFixProvider.cs | 35 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs index b593a23fe17..f381aa31ec0 100644 --- a/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs +++ b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs @@ -16,7 +16,7 @@ public static class AnalyzerDiagnostics public static readonly DiagnosticDescriptor ContextIsAField = new DiagnosticDescriptor( "FLAN0002", "Plugin context is a field", - "Plugin context must be a static property instead", + "Plugin context must be at least internal static property", "Localization", DiagnosticSeverity.Error, isEnabledByDefault: true @@ -25,7 +25,7 @@ public static class AnalyzerDiagnostics public static readonly DiagnosticDescriptor ContextIsNotStatic = new DiagnosticDescriptor( "FLAN0003", "Plugin context is not static", - "Plugin context must be a static property", + "Plugin context must be at least internal static property", "Localization", DiagnosticSeverity.Error, isEnabledByDefault: true @@ -34,7 +34,7 @@ public static class AnalyzerDiagnostics public static readonly DiagnosticDescriptor ContextAccessIsTooRestrictive = new DiagnosticDescriptor( "FLAN0004", "Plugin context property access modifier is too restrictive", - "Plugin context property must be internal or public", + "Plugin context property must be at least internal static property", "Localization", DiagnosticSeverity.Error, isEnabledByDefault: true @@ -43,7 +43,7 @@ public static class AnalyzerDiagnostics public static readonly DiagnosticDescriptor ContextIsNotDeclared = new DiagnosticDescriptor( "FLAN0005", "Plugin context is not declared", - "Plugin context must be a static property of type `PluginInitContext`", + "Plugin context must be at least internal static property of type `PluginInitContext`", "Localization", DiagnosticSeverity.Error, isEnabledByDefault: true diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs index fef040f40a4..9b798018a21 100644 --- a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs +++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs @@ -123,7 +123,7 @@ private static Document FixContextIsNotStaticError(CodeFixContext context, Synta var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); if (propertyDeclaration is null) return context.Document; - var newPropertyDeclaration = propertyDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + var newPropertyDeclaration = FixRestrictivePropertyModifiers(propertyDeclaration).AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); return context.Document.WithSyntaxRoot(newRoot); @@ -134,20 +134,7 @@ private static Document FixContextIsTooRestricted(CodeFixContext context, Syntax var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); if (propertyDeclaration is null) return context.Document; - var newModifiers = SyntaxFactory.TokenList(); - foreach (var modifier in propertyDeclaration.Modifiers) - { - if (modifier.IsKind(SyntaxKind.PrivateKeyword) || modifier.IsKind(SyntaxKind.ProtectedKeyword)) - { - newModifiers = newModifiers.Add(SyntaxFactory.Token(SyntaxKind.InternalKeyword)); - } - else - { - newModifiers = newModifiers.Add(modifier); - } - } - - var newPropertyDeclaration = propertyDeclaration.WithModifiers(newModifiers); + var newPropertyDeclaration = FixRestrictivePropertyModifiers(propertyDeclaration); var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); return context.Document.WithSyntaxRoot(newRoot); @@ -172,6 +159,24 @@ private static Document FixContextIsAFieldError(CodeFixContext context, SyntaxNo return GetFormattedDocument(context, newRoot); } + private static PropertyDeclarationSyntax FixRestrictivePropertyModifiers(PropertyDeclarationSyntax propertyDeclaration) + { + var newModifiers = SyntaxFactory.TokenList(); + foreach (var modifier in propertyDeclaration.Modifiers) + { + if (modifier.IsKind(SyntaxKind.PrivateKeyword) || modifier.IsKind(SyntaxKind.ProtectedKeyword)) + { + newModifiers = newModifiers.Add(SyntaxFactory.Token(SyntaxKind.InternalKeyword)); + } + else + { + newModifiers = newModifiers.Add(modifier); + } + } + + return propertyDeclaration.WithModifiers(newModifiers); + } + private static T GetDeclarationSyntax(SyntaxNode root, TextSpan diagnosticSpan) where T : SyntaxNode => root .FindToken(diagnosticSpan.Start)