Skip to content

Move PluginClassInfo & Add code regions #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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

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

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

View workflow job for this annotation

GitHub Actions / build

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

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

View workflow job for this annotation

GitHub Actions / build

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

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
AnalyzerDiagnostics.ContextIsAField,
AnalyzerDiagnostics.ContextIsNotStatic,
Expand All @@ -25,47 +27,55 @@
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<PropertyDeclarationSyntax>()
.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<FieldDeclarationSyntax>()
.SelectMany(f => f.Declaration.Variables)
Expand All @@ -75,7 +85,6 @@
?.DeclaringSyntaxReferences[0]
.GetSyntax()
.FirstAncestorOrSelf<FieldDeclarationSyntax>();

if (parentSyntax != null)
{
context.ReportDiagnostic(Diagnostic.Create(
Expand All @@ -85,13 +94,13 @@
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> FixableDiagnosticIds => ImmutableArray.Create(
AnalyzerDiagnostics.ContextIsAField.Id,
AnalyzerDiagnostics.ContextIsNotStatic.Id,
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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();
Expand All @@ -185,5 +193,9 @@ private static T GetDeclarationSyntax<T>(SyntaxNode root, TextSpan diagnosticSpa
?.AncestorsAndSelf()
.OfType<T>()
.First();

#endregion

#endregion
}
}
29 changes: 29 additions & 0 deletions Flow.Launcher.Localization.Shared/Classes.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.13.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
</ItemGroup>

</Project>
70 changes: 69 additions & 1 deletion Flow.Launcher.Localization.Shared/Helper.cs
Original file line number Diff line number Diff line change
@@ -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) ||
Expand All @@ -13,5 +20,66 @@ public static bool GetFLLUseDependencyInjection(this AnalyzerConfigOptionsProvid
}
return useDI;
}

#endregion

#region Plugin Class Info

/// <summary>
/// If cannot find the class that implements IPluginI18n, return null.
/// If cannot find the context property, return PluginClassInfo with null context property name.
/// </summary>
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<PropertyDeclarationSyntax>()
.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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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<PropertyDeclarationSyntax>()
.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<PluginClassInfo> pluginClasses,
SourceProductionContext context,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -772,29 +737,6 @@ public LocalizableString(string key, string value, string summary, IEnumerable<L
}
}

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 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 = location;
ClassName = className;
PropertyName = propertyName;
IsStatic = isStatic;
IsPrivate = isPrivate;
IsProtected = isProtected;
}
}

#endregion
}
}
Loading