diff --git a/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzer.cs b/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzer.cs index 72a2775..fa9da76 100644 --- a/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzer.cs +++ b/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzer.cs @@ -11,6 +11,8 @@ namespace Flow.Launcher.Localization.Analyzers.Localize [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ContextAvailabilityAnalyzer : DiagnosticAnalyzer { + #region DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( AnalyzerDiagnostics.ContextIsAField, AnalyzerDiagnostics.ContextIsNotStatic, @@ -25,47 +27,55 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); } + #endregion + + #region Analyze Methods + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) { var configOptions = context.Options.AnalyzerConfigOptionsProvider; var useDI = configOptions.GetFLLUseDependencyInjection(); - - // If we use dependency injection, we don't need to check for this context property - if (useDI) return; + if (useDI) + { + // If we use dependency injection, we don't need to check for this context property + return; + } 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?.Type.Name is Constants.PluginContextTypeName); + var pluginClassInfo = Helper.GetPluginClassInfo(classDeclaration, semanticModel, context.CancellationToken); + if (pluginClassInfo == null) + { + // Cannot find class that implements IPluginI18n + return; + } - if (contextProperty != null) + // Context property is found, check if it's a valid property + if (pluginClassInfo.PropertyName != null) { - if (!contextProperty.IsStatic) + if (!pluginClassInfo.IsStatic) { context.ReportDiagnostic(Diagnostic.Create( AnalyzerDiagnostics.ContextIsNotStatic, - contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + pluginClassInfo.CodeFixLocation )); return; } - if (contextProperty.DeclaredAccessibility is Accessibility.Private || contextProperty.DeclaredAccessibility is Accessibility.Protected) + if (pluginClassInfo.IsPrivate || pluginClassInfo.IsProtected) { context.ReportDiagnostic(Diagnostic.Create( AnalyzerDiagnostics.ContextAccessIsTooRestrictive, - contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + pluginClassInfo.CodeFixLocation )); return; } + // If the context property is valid, we don't need to check for anything else return; } + // Context property is not found, check if it's declared as a field var fieldDeclaration = classDeclaration.Members .OfType() .SelectMany(f => f.Declaration.Variables) @@ -75,7 +85,6 @@ private static void AnalyzeNode(SyntaxNodeAnalysisContext context) ?.DeclaringSyntaxReferences[0] .GetSyntax() .FirstAncestorOrSelf(); - if (parentSyntax != null) { context.ReportDiagnostic(Diagnostic.Create( @@ -85,13 +94,13 @@ private static void AnalyzeNode(SyntaxNodeAnalysisContext context) return; } + // Context property is not found, report an error context.ReportDiagnostic(Diagnostic.Create( AnalyzerDiagnostics.ContextIsNotDeclared, classDeclaration.Identifier.GetLocation() )); } - private static bool IsPluginEntryClass(INamedTypeSymbol namedTypeSymbol) => - namedTypeSymbol?.Interfaces.Any(i => i.Name == Constants.PluginInterfaceName) ?? false; + #endregion } } diff --git a/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs b/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs index 54df68a..8ef920c 100644 --- a/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs +++ b/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs @@ -17,6 +17,8 @@ namespace Flow.Launcher.Localization.Analyzers.Localize [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ContextAvailabilityAnalyzerCodeFixProvider)), Shared] public class ContextAvailabilityAnalyzerCodeFixProvider : CodeFixProvider { + #region CodeFixProvider + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( AnalyzerDiagnostics.ContextIsAField.Id, AnalyzerDiagnostics.ContextIsNotStatic.Id, @@ -82,21 +84,9 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) } } - private static MemberDeclarationSyntax GetStaticContextPropertyDeclaration(string propertyName = "Context") => - SyntaxFactory.ParseMemberDeclaration( - $"internal static {Constants.PluginContextTypeName} {propertyName} {{ get; private set; }} = null!;" - ); + #endregion - 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); - } + #region Fix Methods private static Document FixContextNotDeclared(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) { @@ -160,6 +150,24 @@ private static Document FixContextIsAFieldError(CodeFixContext context, SyntaxNo return GetFormattedDocument(context, newRoot); } + #region Utils + + private static MemberDeclarationSyntax GetStaticContextPropertyDeclaration(string propertyName = "Context") => + SyntaxFactory.ParseMemberDeclaration( + $"internal static {Constants.PluginContextTypeName} {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 static PropertyDeclarationSyntax FixRestrictivePropertyModifiers(PropertyDeclarationSyntax propertyDeclaration) { var newModifiers = SyntaxFactory.TokenList(); @@ -185,5 +193,9 @@ private static T GetDeclarationSyntax(SyntaxNode root, TextSpan diagnosticSpa ?.AncestorsAndSelf() .OfType() .First(); + + #endregion + + #endregion } } diff --git a/Flow.Launcher.Localization.Shared/Classes.cs b/Flow.Launcher.Localization.Shared/Classes.cs new file mode 100644 index 0000000..9c78d98 --- /dev/null +++ b/Flow.Launcher.Localization.Shared/Classes.cs @@ -0,0 +1,29 @@ +using Microsoft.CodeAnalysis; + +namespace Flow.Launcher.Localization.Shared +{ + public class PluginClassInfo + { + public Location Location { get; } + public string ClassName { get; } + public string PropertyName { get; } + public bool IsStatic { get; } + public bool IsPrivate { get; } + public bool IsProtected { get; } + public Location CodeFixLocation { get; } + + public string ContextAccessor => $"{ClassName}.{PropertyName}"; + public bool IsValid => PropertyName != null && IsStatic && (!IsPrivate) && (!IsProtected); + + public PluginClassInfo(Location location, string className, string propertyName, bool isStatic, bool isPrivate, bool isProtected, Location codeFixLocation) + { + Location = location; + ClassName = className; + PropertyName = propertyName; + IsStatic = isStatic; + IsPrivate = isPrivate; + IsProtected = isProtected; + CodeFixLocation = codeFixLocation; + } + } +} diff --git a/Flow.Launcher.Localization.Shared/Flow.Launcher.Localization.Shared.csproj b/Flow.Launcher.Localization.Shared/Flow.Launcher.Localization.Shared.csproj index ae15976..74cca20 100644 --- a/Flow.Launcher.Localization.Shared/Flow.Launcher.Localization.Shared.csproj +++ b/Flow.Launcher.Localization.Shared/Flow.Launcher.Localization.Shared.csproj @@ -8,7 +8,7 @@ - + diff --git a/Flow.Launcher.Localization.Shared/Helper.cs b/Flow.Launcher.Localization.Shared/Helper.cs index 94b186c..d9502ac 100644 --- a/Flow.Launcher.Localization.Shared/Helper.cs +++ b/Flow.Launcher.Localization.Shared/Helper.cs @@ -1,9 +1,16 @@ -using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Linq; +using System.Threading; namespace Flow.Launcher.Localization.Shared { public static class Helper { + #region Build Properties + public static bool GetFLLUseDependencyInjection(this AnalyzerConfigOptionsProvider configOptions) { if (!configOptions.GlobalOptions.TryGetValue("build_property.FLLUseDependencyInjection", out var result) || @@ -13,5 +20,66 @@ public static bool GetFLLUseDependencyInjection(this AnalyzerConfigOptionsProvid } return useDI; } + + #endregion + + #region Plugin Class Info + + /// + /// If cannot find the class that implements IPluginI18n, return null. + /// If cannot find the context property, return PluginClassInfo with null context property name. + /// + public static PluginClassInfo GetPluginClassInfo(ClassDeclarationSyntax classDecl, SemanticModel semanticModel, CancellationToken ct) + { + var classSymbol = semanticModel.GetDeclaredSymbol(classDecl, ct); + if (!IsPluginEntryClass(classSymbol)) + { + // Cannot find class that implements IPluginI18n + return null; + } + + var property = GetContextProperty(classDecl); + var location = GetLocation(semanticModel.SyntaxTree, classDecl); + if (property is null) + { + // Cannot find context + return new PluginClassInfo(location, classDecl.Identifier.Text, null, false, false, false, null); + } + + var modifiers = property.Modifiers; + var codeFixLocation = GetCodeFixLocation(property, semanticModel); + return new PluginClassInfo( + location, + classDecl.Identifier.Text, + property.Identifier.Text, + modifiers.Any(SyntaxKind.StaticKeyword), + modifiers.Any(SyntaxKind.PrivateKeyword), + modifiers.Any(SyntaxKind.ProtectedKeyword), + codeFixLocation); + } + + private static bool IsPluginEntryClass(INamedTypeSymbol namedTypeSymbol) + { + return namedTypeSymbol?.Interfaces.Any(i => i.Name == Constants.PluginInterfaceName) ?? false; + } + + private static PropertyDeclarationSyntax GetContextProperty(ClassDeclarationSyntax classDecl) + { + return classDecl.Members + .OfType() + .FirstOrDefault(p => p.Type.ToString() == Constants.PluginContextTypeName); + } + + private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration) + { + return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan); + } + + private static Location GetCodeFixLocation(PropertyDeclarationSyntax property, SemanticModel semanticModel) + { + return semanticModel.GetDeclaredSymbol(property).DeclaringSyntaxReferences[0].GetSyntax().GetLocation(); + } + + #endregion } } diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs index 9f797c8..0ef37fa 100644 --- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs +++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs @@ -7,7 +7,6 @@ using System.Xml.Linq; using Flow.Launcher.Localization.Shared; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; @@ -57,7 +56,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var pluginClasses = context.SyntaxProvider .CreateSyntaxProvider( predicate: (n, _) => n is ClassDeclarationSyntax, - transform: GetPluginClassInfo) + transform: (c, t) => Helper.GetPluginClassInfo((ClassDeclarationSyntax)c.Node, c.SemanticModel, t)) .Where(info => info != null) .Collect(); @@ -428,35 +427,6 @@ private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext co #region Get Plugin Class Info - private static PluginClassInfo GetPluginClassInfo(GeneratorSyntaxContext context, CancellationToken ct) - { - var classDecl = (ClassDeclarationSyntax)context.Node; - var location = GetLocation(context.SemanticModel.SyntaxTree, classDecl); - if (!classDecl.BaseList?.Types.Any(t => t.Type.ToString() == Constants.PluginInterfaceName) ?? true) - { - // Cannot find class that implements IPluginI18n - return null; - } - - var property = classDecl.Members - .OfType() - .FirstOrDefault(p => p.Type.ToString() == Constants.PluginContextTypeName); - if (property is null) - { - // Cannot find context - return new PluginClassInfo(location, classDecl.Identifier.Text, null, false, false, false); - } - - var modifiers = property.Modifiers; - return new PluginClassInfo( - location, - classDecl.Identifier.Text, - property.Identifier.Text, - modifiers.Any(SyntaxKind.StaticKeyword), - modifiers.Any(SyntaxKind.PrivateKeyword), - modifiers.Any(SyntaxKind.ProtectedKeyword)); - } - private static PluginClassInfo GetValidPluginInfo( ImmutableArray pluginClasses, SourceProductionContext context, @@ -535,11 +505,6 @@ private static PluginClassInfo GetValidPluginInfo( return null; } - private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration) - { - return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan); - } - #endregion #region Generate Source @@ -772,29 +737,6 @@ public LocalizableString(string key, string value, string summary, IEnumerable $"{ClassName}.{PropertyName}"; - public bool IsValid => PropertyName != null && IsStatic && (!IsPrivate) && (!IsProtected); - - public PluginClassInfo(Location location, string className, string propertyName, bool isStatic, bool isPrivate, bool isProtected) - { - Location = location; - ClassName = className; - PropertyName = propertyName; - IsStatic = isStatic; - IsPrivate = isPrivate; - IsProtected = isProtected; - } - } - #endregion } }