diff --git a/Stl.Fusion.sln b/Stl.Fusion.sln index 1d5bcf912..0098d720d 100644 --- a/Stl.Fusion.sln +++ b/Stl.Fusion.sln @@ -114,6 +114,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniRpc", "samples\MiniRpc\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiServerRpc", "samples\MultiServerRpc\MultiServerRpc.csproj", "{DDD38FE8-3105-44E6-9128-921B0713FF64}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stl.Analyzers", "src\Stl.Analyzers\Stl.Analyzers.csproj", "{A36C2D5F-65EA-4170-ABE4-7CD9A15B381E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -250,6 +252,10 @@ Global {DDD38FE8-3105-44E6-9128-921B0713FF64}.Debug|Any CPU.Build.0 = Debug|Any CPU {DDD38FE8-3105-44E6-9128-921B0713FF64}.Release|Any CPU.ActiveCfg = Release|Any CPU {DDD38FE8-3105-44E6-9128-921B0713FF64}.Release|Any CPU.Build.0 = Release|Any CPU + {A36C2D5F-65EA-4170-ABE4-7CD9A15B381E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A36C2D5F-65EA-4170-ABE4-7CD9A15B381E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A36C2D5F-65EA-4170-ABE4-7CD9A15B381E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A36C2D5F-65EA-4170-ABE4-7CD9A15B381E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -289,6 +295,7 @@ Global {5A8530A3-E69B-4BD3-B7A7-01A916D166EB} = {A50B98CF-9AA1-4622-B2AD-5E17610115A7} {99131CAD-DDC3-4B1A-ABB8-63E0FF32B03E} = {61E78666-E338-4F6B-8CD8-8CD5E13B7C54} {DDD38FE8-3105-44E6-9128-921B0713FF64} = {61E78666-E338-4F6B-8CD8-8CD5E13B7C54} + {A36C2D5F-65EA-4170-ABE4-7CD9A15B381E} = {A50B98CF-9AA1-4622-B2AD-5E17610115A7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B50610D6-2B28-4258-98A2-A0B486FD5907} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index bc217e92a..63cf3ca17 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -50,7 +50,7 @@ - + diff --git a/src/Stl.Analyzers/CommandHandlerAnalyzer.cs b/src/Stl.Analyzers/CommandHandlerAnalyzer.cs new file mode 100644 index 000000000..065a1a6d9 --- /dev/null +++ b/src/Stl.Analyzers/CommandHandlerAnalyzer.cs @@ -0,0 +1,71 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Stl.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class CommandHandlerAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor DirectCallDiagnostic = + new( + "STLC0001", + "Invalid command handler call", + "Direct command handler calls on command service proxies are not allowed", + "Test", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DirectCallDiagnostic); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(CheckCommandHandler, SyntaxKind.InvocationExpression); + } + + private void CheckCommandHandler(SyntaxNodeAnalysisContext context) + { + if (context.Node is not InvocationExpressionSyntax invocation) + return; + + if (context.SemanticModel.GetSymbolInfo(invocation, cancellationToken: context.CancellationToken).Symbol is not IMethodSymbol methodSymbol) + return; + + var syntaxReference = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault(); + + if (syntaxReference is null) + return; + + SyntaxNode declaration = syntaxReference.GetSyntax(context.CancellationToken); + + if (declaration is not MethodDeclarationSyntax method) + return; + + // check if the called method has the CommandHandler attribute + // this is a very naive implementation, the namespace should be checked as well + bool isCommandHandler = method.AttributeLists + .SelectMany(x => x.Attributes) + .Any( + attribute => + string.Equals( + attribute.Name.ToString(), + "CommandHandler", + StringComparison.Ordinal + ) + ); + + if (!isCommandHandler) + return; + + var diagnostic = Diagnostic.Create(DirectCallDiagnostic, context.Node.GetLocation()); + + context.ReportDiagnostic(diagnostic); + } +} diff --git a/src/Stl.Analyzers/Stl.Analyzers.csproj b/src/Stl.Analyzers/Stl.Analyzers.csproj new file mode 100644 index 000000000..af298d400 --- /dev/null +++ b/src/Stl.Analyzers/Stl.Analyzers.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0;net7.0 + enable + enable + true + Stl.Analyzers - Roslyn analyzers for common issues. + + + + + + + +