From b366ebfc3055609c2d434900f5e2f80f2594480b Mon Sep 17 00:00:00 2001 From: Nils Gruson Date: Fri, 10 Jan 2025 18:08:25 +0100 Subject: [PATCH 1/4] Add Dapr.Actors support and diagnostic analyzer - Introduced `Dapr.Actors` package reference (v1.15.0-rc01). - Added `Microsoft.CodeAnalysis.CSharp.Analyzer.Testing` package reference (v1.1.2). - Created `Dapr.Actors.Analyzers` and `Dapr.Actors.Analyzers.Test` projects. - Implemented `ActorRegistrationAnalyzer` to warn about unregistered actors. - Developed `ActorRegistrationCodeFixProvider` to suggest actor registration. - Added unit tests for both the analyzer and code fix provider. - Updated documentation for analyzer releases. - Enhanced test project with necessary references and utility methods. Signed-off-by: Nils Gruson --- Directory.Packages.props | 4 +- all.sln | 14 ++ .../ActorRegistrationAnalyzer.cs | 115 +++++++++ .../ActorRegistrationCodeFixProvider.cs | 227 ++++++++++++++++++ .../AnalyzerReleases.Shipped.md | 7 + .../AnalyzerReleases.Unshipped.md | 3 + .../Dapr.Actors.Analyzers.csproj | 43 ++++ .../ActorRegistrationAnalyzerTests.cs | 174 ++++++++++++++ .../ActorRegistrationCodeFixProviderTests.cs | 161 +++++++++++++ .../Dapr.Actors.Analyzers.Test.csproj | 35 +++ test/Dapr.Actors.Analyzers.Test/Utilities.cs | 57 +++++ .../VerifyAnalyzer.cs | 65 +++++ .../VerifyCodeFix.cs | 56 +++++ 13 files changed, 960 insertions(+), 1 deletion(-) create mode 100644 src/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs create mode 100644 src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs create mode 100644 src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj create mode 100644 test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj create mode 100644 test/Dapr.Actors.Analyzers.Test/Utilities.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index efb48fcc4..1a7179428 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + @@ -21,6 +22,7 @@ + @@ -49,4 +51,4 @@ - + \ No newline at end of file diff --git a/all.sln b/all.sln index 9a163b1d9..ad2443dc0 100644 --- a/all.sln +++ b/all.sln @@ -155,6 +155,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers", "src\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers.csproj", "{D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Test", "test\Dapr.Actors.Analyzers.Test\Dapr.Actors.Analyzers.Test.csproj", "{65FC7DEA-B6DF-4542-8459-90B9FDAEA541}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -403,6 +407,14 @@ Global {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU + {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43}.Release|Any CPU.Build.0 = Release|Any CPU + {65FC7DEA-B6DF-4542-8459-90B9FDAEA541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65FC7DEA-B6DF-4542-8459-90B9FDAEA541}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65FC7DEA-B6DF-4542-8459-90B9FDAEA541}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65FC7DEA-B6DF-4542-8459-90B9FDAEA541}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -477,6 +489,8 @@ Global {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {D562AAFD-1724-4CC7-8CAA-B3AD9D3EAF43} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {65FC7DEA-B6DF-4542-8459-90B9FDAEA541} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs b/src/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs new file mode 100644 index 000000000..9eb130a54 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs @@ -0,0 +1,115 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Actors.Analyzers; + +/// +/// Analyzes actor registration in Dapr applications. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class ActorRegistrationAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor DiagnosticDescriptor = new( + "DAPR0001", + "Actor class not registered", + "The actor class '{0}' is not registered", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// Gets the supported diagnostics for this analyzer. + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptor); + + /// + /// Initializes the analyzer. + /// + /// The analysis context. + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration); + } + + /// + /// Analyzes the actor registration. + /// + /// The syntax node analysis context. + private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + if (classDeclaration.BaseList != null) + { + var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type; + + if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is INamedTypeSymbol baseTypeSymbol) + { + var baseTypeName = baseTypeSymbol.ToDisplayString(); + if (baseTypeName == "Dapr.Actors.Runtime.Actor" || baseTypeName == "Actor") + { + var actorTypeName = classDeclaration.Identifier.Text; + bool isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel); + if (!isRegistered) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptor, classDeclaration.Identifier.GetLocation(), actorTypeName); + context.ReportDiagnostic(diagnostic); + } + } + } + } + } + + /// + /// Checks if the actor is registered. + /// + /// The name of the actor type. + /// The semantic model. + /// True if the actor is registered, otherwise false. + private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel semanticModel) + { + var methodInvocations = new List(); + foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + methodInvocations.AddRange(root.DescendantNodes().OfType()); + } + + foreach (var invocation in methodInvocations) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + continue; + } + + var methodName = memberAccess.Name.Identifier.Text; + if (methodName == "RegisterActor") + { + if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0) + { + if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument) + { + if (typeArgument.Identifier.Text == actorTypeName) + { + return true; + } + } + else if (typeArgumentList.TypeArgumentList.Arguments[0] is QualifiedNameSyntax qualifiedName) + { + if (qualifiedName.Right.Identifier.Text == actorTypeName) + { + return true; + } + } + } + } + } + + return false; + } +} diff --git a/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs new file mode 100644 index 000000000..c5c2e2c2f --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs @@ -0,0 +1,227 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; + +namespace Dapr.Actors.Analyzers; + +/// +/// Provides code fixes for actor registration issues. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))] +[Shared] +public class ActorRegistrationCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0001"); + + /// + /// Registers code fixes for the specified diagnostics. + /// + /// The context to register the code fixes. + /// A task representing the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = "Register actor"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => RegisterActorAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + /// + /// Registers the actor in the specified document. + /// + /// The document to update. + /// The diagnostic to fix. + /// A cancellation token. + /// The updated document. + private async Task RegisterActorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var classDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + + if (root == null || classDeclaration == null) + return document; + + // Get the semantic model + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + + if (semanticModel == null) + return document; + + // Get the symbol for the class declaration + + if (semanticModel.GetDeclaredSymbol(classDeclaration, cancellationToken) is not INamedTypeSymbol classSymbol) + return document; + + // Get the fully qualified name + var actorType = classSymbol.ToDisplayString(new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces)); + + if (string.IsNullOrEmpty(actorType)) + return document; + + // Get the compilation + var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + if (compilation == null) + return document; + + (var targetDocument, var addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken); + + if (addActorsInvocation == null) + { + (targetDocument, addActorsInvocation) = await CreateAddActorsInvocation(document.Project, cancellationToken); + } + + if (addActorsInvocation == null) + return document; + + var targetRoot = await addActorsInvocation.SyntaxTree.GetRootAsync(cancellationToken); + + if (targetRoot == null || targetDocument == null) + return document; + + // Find the options lambda block + var optionsLambda = addActorsInvocation?.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock) + return document; + + // Extract the parameter name from the lambda expression + var parameterName = optionsLambda.Parameter.Identifier.Text; + + // Create the new workflow registration statement + var registerWorkflowStatement = SyntaxFactory.ParseStatement($"{parameterName}.Actors.RegisterActor<{actorType}>();"); + + // Add the new registration statement to the options block + var newOptionsBlock = optionsBlock.AddStatements(registerWorkflowStatement); + + // Replace the old options block with the new one + var newRoot = targetRoot?.ReplaceNode(optionsBlock, newOptionsBlock); + + // Format the new root. + newRoot = Formatter.Format(newRoot!, document.Project.Solution.Workspace); + + return targetDocument.WithSyntaxRoot(newRoot); + } + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + + var addActorsInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + if (addActorsInvocation != null) + { + var document = project.GetDocument(addActorsInvocation.SyntaxTree); + return (document, addActorsInvocation); + } + } + + return (null, null); + } + + private async Task FindCreateBuilderInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + // Find the invocation expression for WebApplication.CreateBuilder() + var createBuilderInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "WebApplication" && + memberAccess.Name.Identifier.Text == "CreateBuilder"); + + if (createBuilderInvocation != null) + { + return createBuilderInvocation; + } + } + + return null; + } + + private async Task<(Document?, InvocationExpressionSyntax?)> CreateAddActorsInvocation(Project project, CancellationToken cancellationToken) + { + var createBuilderInvocation = await FindCreateBuilderInvocationAsync(project, cancellationToken); + + var variableDeclarator = createBuilderInvocation?.Ancestors() + .OfType() + .FirstOrDefault(); + + var builderVariable = variableDeclarator?.Identifier.Text; + + if (createBuilderInvocation != null) + { + var targetRoot = await createBuilderInvocation.SyntaxTree.GetRootAsync(cancellationToken); + var document = project.GetDocument(createBuilderInvocation.SyntaxTree); + + if (createBuilderInvocation.Expression is MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax builderIdentifier }) + { + var addActorsStatement = SyntaxFactory.ParseStatement($"{builderVariable}.Services.AddActors(options => {{ }});"); + + if (createBuilderInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var firstChild = parentBlock.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax); + var newParentBlock = parentBlock.InsertNodesAfter(firstChild, new[] { addActorsStatement }); + targetRoot = targetRoot.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + var firstChild = compilationUnitSyntax.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax); + var globalStatement = SyntaxFactory.GlobalStatement(addActorsStatement); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(firstChild, new[] { globalStatement }); + targetRoot = targetRoot.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); + } + + var addActorsInvocation = targetRoot?.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + return (document, addActorsInvocation); + } + } + + return (null, null); + } +} diff --git a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..cea2f6b81 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,7 @@ +## Release 1.16 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DAPR0001| Usage | Warning | The actor class '{0}' is not registered \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..b1b99aaf2 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,3 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj new file mode 100644 index 000000000..910ecb295 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj @@ -0,0 +1,43 @@ + + + + netstandard2.0 + + enable + enable + true + + + + + + + + + true + + + false + + + false + + + This package contains Roslyn analyzers for actors. + $(PackageTags) + + + + + + + + + + + + + + + + diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs new file mode 100644 index 000000000..9beda716e --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs @@ -0,0 +1,174 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Analyzers.Test; + +public class ActorRegistrationAnalyzerTests +{ + public class ActorNotRegistered + { + [Fact] + public async Task ReportDiagnostic_DAPR0001() + { + var testCode = @" + using Dapr.Actors.Runtime; + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) + .WithSpan(4, 23, 4, 32).WithMessage("The actor class 'TestActor' is not registered"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportDiagnostic_DAPR0001_FullyQualified() + { + var testCode = @" + class TestActor : Dapr.Actors.Runtime.Actor + { + public TestActor(Dapr.Actors.Runtime.ActorHost host) : base(host) + { + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) + .WithSpan(2, 23, 2, 32).WithMessage("The actor class 'TestActor' is not registered"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportDiagnostic_DAPR0001_NamespaceAlias() + { + var testCode = @" + using alias = Dapr.Actors.Runtime; + + class TestActor : alias.Actor + { + public TestActor(alias.ActorHost host) : base(host) + { + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) + .WithSpan(4, 23, 4, 32).WithMessage("The actor class 'TestActor' is not registered"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + } + + public class ActorRegistered + { + [Fact] + public async Task ReportNoDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var startupCode = @" + using Microsoft.Extensions.DependencyInjection; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, startupCode); + } + + [Fact] + public async Task ReportNoDiagnostic_WithNamespace() + { + var testCode = @" + using Dapr.Actors.Runtime; + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + var startupCode = @" + using Microsoft.Extensions.DependencyInjection; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, startupCode); + } + + [Fact] + public async Task ReportNoDiagnostic_WithNamespaceAlias () + { + var testCode = @" + using Dapr.Actors.Runtime; + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + var startupCode = @" + using Microsoft.Extensions.DependencyInjection; + using alias = TestNamespace; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, startupCode); + } + } + +} diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs new file mode 100644 index 000000000..034e68485 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs @@ -0,0 +1,161 @@ +namespace Dapr.Actors.Analyzers.Test; + +public class ActorRegistrationCodeFixProviderTests +{ + [Fact] + public async Task RegisterActor() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + services.AddActors(options => + { + }); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_WhenAddActorsIsNotFound() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + + var app = builder.Build(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_WhenAddActorsIsNotFound_TopLevelStatements() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + + var app = builder.Build(); + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj new file mode 100644 index 000000000..934fcbfdd --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj @@ -0,0 +1,35 @@ + + + + enable + enable + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/test/Dapr.Actors.Analyzers.Test/Utilities.cs b/test/Dapr.Actors.Analyzers.Test/Utilities.cs new file mode 100644 index 000000000..dad3456c0 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/Utilities.cs @@ -0,0 +1,57 @@ +using System.Collections.Immutable; +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; + +namespace Dapr.Actors.Analyzers.Test; + +internal static class Utilities +{ + public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code) + { + var workspace = new AdhocWorkspace(); + + // Create a new project with necessary references + var project = workspace.AddProject("TestProject", LanguageNames.CSharp) + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication))); + + // Add the document to the project + var document = project.AddDocument("TestDocument.cs", code); + + // Get the syntax tree and create a compilation + var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null"); + var compilation = CSharpCompilation.Create("TestCompilation") + .AddSyntaxTrees(syntaxTree) + .AddReferences(project.MetadataReferences); + + var compilationWithAnalyzer = compilation.WithAnalyzers( + ImmutableArray.Create( + new ActorRegistrationAnalyzer())); + + // Get diagnostics from the compilation + var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync(); + return (diagnostics, document, workspace); + } + + public static MetadataReference[] GetAllReferencesNeededForType(Type type) + { + var files = GetAllAssemblyFilesNeededForType(type); + + return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray(); + } + + private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) + { + return type.Assembly.GetReferencedAssemblies() + .Select(x => Assembly.Load(x.FullName)) + .Append(type.Assembly) + .Select(x => x.Location) + .ToImmutableArray(); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs new file mode 100644 index 000000000..ea45924dd --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs @@ -0,0 +1,65 @@ +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Dapr.Actors.Runtime; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Actors.Analyzers.Test; + +internal static class VerifyAnalyzer +{ + public static DiagnosticResult Diagnostic(string diagnosticId, DiagnosticSeverity diagnosticSeverity) + { + return new DiagnosticResult(diagnosticId, diagnosticSeverity); + } + + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + await VerifyAnalyzerAsync(source, null, expected); + } + + public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) + { + var test = new Test { TestCode = source }; + +#if NET6_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; +#elif NET7_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net70; +#elif NET8_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + + if (program != null) + { + test.TestState.Sources.Add(("Program.cs", program)); + } + + var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList(); + metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + foreach (var reference in metadataReferences) + { + test.TestState.AdditionalReferences.Add(reference); + } + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + private class Test : CSharpAnalyzerTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!; + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + return solution; + }); + } + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs new file mode 100644 index 000000000..f7d0d7eb4 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs @@ -0,0 +1,56 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Dapr.Actors.Analyzers.Test; + +internal class VerifyCodeFix +{ + public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new() + { + var (diagnostics, document, workspace) = await Utilities.GetDiagnosticsAdvanced(code); + + Assert.Single(diagnostics); + + var diagnostic = diagnostics[0]; + + var codeFixProvider = new T(); + + CodeAction? registeredCodeAction = null; + + var context = new CodeFixContext(document, diagnostic, (codeAction, _) => + { + if (registeredCodeAction != null) + throw new Exception("Code action was registered more than once"); + + registeredCodeAction = codeAction; + + }, CancellationToken.None); + + await codeFixProvider.RegisterCodeFixesAsync(context); + + if (registeredCodeAction == null) + throw new Exception("Code action was not registered"); + + var operations = await registeredCodeAction.GetOperationsAsync(CancellationToken.None); + + foreach (var operation in operations) + { + operation.Apply(workspace, CancellationToken.None); + } + + var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null"); + var newCode = (await updatedDocument.GetTextAsync()).ToString(); + + // Normalize whitespace + string NormalizeWhitespace(string input) + { + var separator = new[] { ' ', '\r', '\n' }; + return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries)); + } + + var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode); + var normalizedNewCode = NormalizeWhitespace(newCode); + + Assert.Equal(normalizedExpectedCode, normalizedNewCode); + } +} From 7d512ec9f72d5ef632f0947b32edf495fa95f3ae Mon Sep 17 00:00:00 2001 From: Nils Gruson Date: Mon, 13 Jan 2025 10:53:55 +0100 Subject: [PATCH 2/4] Refactor actor registration analyzer and add JSON support Renamed `ActorRegistrationAnalyzer` to `ActorAnalyzer` and enhanced it to analyze JSON serialization options for actors. Updated the code fix provider to `ActorJsonSerializationCodeFixProvider` to enable JSON serialization when not set. Introduced new diagnostic `DAPR0002` for interoperability with non-.NET actors. Updated project references and added tests for the new functionality while improving code structure and readability. Signed-off-by: Nils Gruson --- src/Dapr.Actors.Analyzers/ActorAnalyzer.cs | 194 ++++++++++++++++++ .../ActorJsonSerializationCodeFixProvider.cs | 120 +++++++++++ .../ActorRegistrationAnalyzer.cs | 115 ----------- .../ActorRegistrationCodeFixProvider.cs | 13 +- .../AnalyzerReleases.Shipped.md | 3 +- src/Dapr.Actors/Dapr.Actors.csproj | 1 + ...AnalyzerTests.cs => ActorAnalyzerTests.cs} | 71 ++++++- ...orJsonSerializationCodeFixProviderTests.cs | 43 ++++ .../ActorRegistrationCodeFixProviderTests.cs | 2 + test/Dapr.Actors.Analyzers.Test/Utilities.cs | 2 +- .../VerifyAnalyzer.cs | 4 +- 11 files changed, 437 insertions(+), 131 deletions(-) create mode 100644 src/Dapr.Actors.Analyzers/ActorAnalyzer.cs create mode 100644 src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs delete mode 100644 src/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs rename test/Dapr.Actors.Analyzers.Test/{ActorRegistrationAnalyzerTests.cs => ActorAnalyzerTests.cs} (69%) create mode 100644 test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs diff --git a/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs new file mode 100644 index 000000000..e3f03ac25 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs @@ -0,0 +1,194 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Actors.Analyzers; + +/// +/// Analyzes actor registration in Dapr applications. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class ActorAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor DiagnosticDescriptorActorRegistration = new( + "DAPR0001", + "Actor class not registered", + "The actor class '{0}' is not registered", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor DiagnosticDescriptorJsonSerialization = new( + "DAPR0002", + "Use JsonSerialization", + "Add options.UseJsonSerialization to support interoperability with non-.NET actors", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// Gets the supported diagnostics for this analyzer. + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagnosticDescriptorActorRegistration, DiagnosticDescriptorJsonSerialization); + + /// + /// Initializes the analyzer. + /// + /// The analysis context. + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeSerialization, SyntaxKind.CompilationUnit); + } + + private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + if (classDeclaration.BaseList != null) + { + var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type; + + if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is INamedTypeSymbol baseTypeSymbol) + { + var baseTypeName = baseTypeSymbol.ToDisplayString(); + + { + var actorTypeName = classDeclaration.Identifier.Text; + bool isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel); + if (!isRegistered) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorActorRegistration, classDeclaration.Identifier.GetLocation(), actorTypeName); + context.ReportDiagnostic(diagnostic); + } + } + } + } + } + + private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel semanticModel) + { + var methodInvocations = new List(); + foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + methodInvocations.AddRange(root.DescendantNodes().OfType()); + } + + foreach (var invocation in methodInvocations) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + continue; + } + + var methodName = memberAccess.Name.Identifier.Text; + if (methodName == "RegisterActor") + { + if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0) + { + if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument) + { + if (typeArgument.Identifier.Text == actorTypeName) + { + return true; + } + } + else if (typeArgumentList.TypeArgumentList.Arguments[0] is QualifiedNameSyntax qualifiedName) + { + if (qualifiedName.Right.Identifier.Text == actorTypeName) + { + return true; + } + } + } + } + } + + return false; + } + + private void AnalyzeSerialization(SyntaxNodeAnalysisContext context) + { + var actorTypes = GetActorDerivedClasses(context); + InvocationExpressionSyntax? addActorsInvocation = null; + + if (actorTypes.Any()) + { + foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + addActorsInvocation = root.DescendantNodes().OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + if (addActorsInvocation != null) + { + break; + } + } + + if (addActorsInvocation != null) + { + var optionsLambda = addActorsInvocation.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda != null) + { + var lambdaBody = optionsLambda.Body; + var assignments = lambdaBody.DescendantNodes().OfType(); + + var useJsonSerialization = assignments.Any(assignment => + assignment.Left is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "UseJsonSerialization" && + assignment.Right is LiteralExpressionSyntax literal && + literal.Token.ValueText == "true"); + + if (!useJsonSerialization) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorJsonSerialization, addActorsInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } + } + } + + private static IEnumerable GetActorDerivedClasses(SyntaxNodeAnalysisContext context) + { + var compilation = context.SemanticModel.Compilation; + var actorDerivedClasses = new List(); + + foreach (var syntaxTree in compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + var classDeclarations = root.DescendantNodes().OfType(); + + foreach (var classDeclaration in classDeclarations) + { + if (classDeclaration.BaseList != null) + { + var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type; + + if (baseTypeSyntax is IdentifierNameSyntax identifier) + { + if (identifier.Identifier.Text == "Dapr.Actors.Runtime.Actor" || identifier.Identifier.Text == "Actor") + { + actorDerivedClasses.Add(classDeclaration); + } + } + } + } + } + + return actorDerivedClasses; + } +} diff --git a/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs new file mode 100644 index 000000000..df3e832ce --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs @@ -0,0 +1,120 @@ +using System.Composition; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; + +namespace Dapr.Actors.Analyzers; + +/// +/// Provides code fix to enable JSON serialization for actors. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))] +[Shared] +public class ActorJsonSerializationCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0002"); + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + /// Registers code fixes for the specified diagnostics. + /// + /// The context to register the code fixes. + /// A task representing the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = "Use JSON serialization"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => UseJsonSerializationAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + private async Task UseJsonSerializationAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + (_, var addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken); + + if (addActorsInvocation == null) + { + return document; + } + + var optionsLambda = addActorsInvocation?.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock) + return document; + + // Extract the parameter name from the lambda expression + var parameterName = optionsLambda.Parameter.Identifier.Text; + + // Check if the lambda body already contains the assignment + var assignmentExists = optionsBlock.Statements + .OfType() + .Any(statement => statement.Expression is AssignmentExpressionSyntax assignment && + assignment.Left is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is IdentifierNameSyntax identifier && + identifier.Identifier.Text == parameterName && + memberAccess.Name.Identifier.Text == "UseJsonSerialization"); + + if (!assignmentExists) + { + var assignmentStatement = SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(parameterName), + SyntaxFactory.IdentifierName("UseJsonSerialization")), + SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))); + + var newOptionsBlock = optionsBlock.AddStatements(assignmentStatement); + var root = await document.GetSyntaxRootAsync(cancellationToken); + var newRoot = root?.ReplaceNode(optionsBlock, newOptionsBlock); + return document.WithSyntaxRoot(newRoot!); + } + + return document; + } + + private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + var addActorsInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + if (addActorsInvocation != null) + { + var document = project.GetDocument(addActorsInvocation.SyntaxTree); + return (document, addActorsInvocation); + } + } + + return (null, null); + } +} diff --git a/src/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs b/src/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs deleted file mode 100644 index 9eb130a54..000000000 --- a/src/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace Dapr.Actors.Analyzers; - -/// -/// Analyzes actor registration in Dapr applications. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ActorRegistrationAnalyzer : DiagnosticAnalyzer -{ - private static readonly DiagnosticDescriptor DiagnosticDescriptor = new( - "DAPR0001", - "Actor class not registered", - "The actor class '{0}' is not registered", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - /// - /// Gets the supported diagnostics for this analyzer. - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptor); - - /// - /// Initializes the analyzer. - /// - /// The analysis context. - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration); - } - - /// - /// Analyzes the actor registration. - /// - /// The syntax node analysis context. - private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context) - { - var classDeclaration = (ClassDeclarationSyntax)context.Node; - - if (classDeclaration.BaseList != null) - { - var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type; - - if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is INamedTypeSymbol baseTypeSymbol) - { - var baseTypeName = baseTypeSymbol.ToDisplayString(); - if (baseTypeName == "Dapr.Actors.Runtime.Actor" || baseTypeName == "Actor") - { - var actorTypeName = classDeclaration.Identifier.Text; - bool isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel); - if (!isRegistered) - { - var diagnostic = Diagnostic.Create(DiagnosticDescriptor, classDeclaration.Identifier.GetLocation(), actorTypeName); - context.ReportDiagnostic(diagnostic); - } - } - } - } - } - - /// - /// Checks if the actor is registered. - /// - /// The name of the actor type. - /// The semantic model. - /// True if the actor is registered, otherwise false. - private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel semanticModel) - { - var methodInvocations = new List(); - foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees) - { - var root = syntaxTree.GetRoot(); - methodInvocations.AddRange(root.DescendantNodes().OfType()); - } - - foreach (var invocation in methodInvocations) - { - if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) - { - continue; - } - - var methodName = memberAccess.Name.Identifier.Text; - if (methodName == "RegisterActor") - { - if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0) - { - if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument) - { - if (typeArgument.Identifier.Text == actorTypeName) - { - return true; - } - } - else if (typeArgumentList.TypeArgumentList.Arguments[0] is QualifiedNameSyntax qualifiedName) - { - if (qualifiedName.Right.Identifier.Text == actorTypeName) - { - return true; - } - } - } - } - } - - return false; - } -} diff --git a/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs index c5c2e2c2f..d30d9f5ba 100644 --- a/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs +++ b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs @@ -38,14 +38,7 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) context.Diagnostics); return Task.CompletedTask; } - - /// - /// Registers the actor in the specified document. - /// - /// The document to update. - /// The diagnostic to fix. - /// A cancellation token. - /// The updated document. + private async Task RegisterActorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); @@ -137,12 +130,12 @@ private async Task RegisterActorAsync(Document document, Diagnostic di foreach (var syntaxTree in compilation!.SyntaxTrees) { - var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); var addActorsInvocation = syntaxRoot.DescendantNodes() .OfType() .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && - memberAccess.Name.Identifier.Text == "AddActors"); + memberAccess.Name.Identifier.Text == "AddActors"); if (addActorsInvocation != null) { diff --git a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md index cea2f6b81..d377176c5 100644 --- a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md @@ -4,4 +4,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- -DAPR0001| Usage | Warning | The actor class '{0}' is not registered \ No newline at end of file +DAPR0001| Usage | Warning | The actor class '{0}' is not registered +DAPR0002| Usage | Warning | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors \ No newline at end of file diff --git a/src/Dapr.Actors/Dapr.Actors.csproj b/src/Dapr.Actors/Dapr.Actors.csproj index bcb8d830f..1c12bcd8e 100644 --- a/src/Dapr.Actors/Dapr.Actors.csproj +++ b/src/Dapr.Actors/Dapr.Actors.csproj @@ -7,6 +7,7 @@ + diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs similarity index 69% rename from test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs rename to test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs index 9beda716e..ec5a2f533 100644 --- a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs @@ -2,7 +2,7 @@ namespace Dapr.Actors.Analyzers.Test; -public class ActorRegistrationAnalyzerTests +public class ActorAnalyzerTests { public class ActorNotRegistered { @@ -89,8 +89,9 @@ internal static class Extensions public static void AddApplicationServices(this IServiceCollection services) { services.AddActors(options => - { + { options.Actors.RegisterActor(); + options.UseJsonSerialization = true; }); } } @@ -126,6 +127,7 @@ public static void AddApplicationServices(this IServiceCollection services) services.AddActors(options => { options.Actors.RegisterActor(); + options.UseJsonSerialization = true; }); } } @@ -162,6 +164,7 @@ public static void AddApplicationServices(this IServiceCollection services) services.AddActors(options => { options.Actors.RegisterActor(); + options.UseJsonSerialization = true; }); } } @@ -171,4 +174,68 @@ public static void AddApplicationServices(this IServiceCollection services) } } + public class JsonSerialization + { + [Fact] + public async Task ReportDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.Extensions.DependencyInjection; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0002", DiagnosticSeverity.Warning) + .WithSpan(9, 25, 12, 27).WithMessage("Add options.UseJsonSerialization to support interoperability with non-.NET actors"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportNoDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.Extensions.DependencyInjection; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + } } diff --git a/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs new file mode 100644 index 000000000..db8072ed8 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs @@ -0,0 +1,43 @@ +namespace Dapr.Actors.Analyzers.Test; + +public class ActorJsonSerializationCodeFixProviderTests +{ + [Fact] + public async Task UseJsonSerialization() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + services.AddActors(options => + { + }); + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs index 034e68485..642dba40e 100644 --- a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs @@ -16,6 +16,7 @@ public static void Main() var services = new ServiceCollection(); services.AddActors(options => { + options.UseJsonSerialization = true; }); } } @@ -39,6 +40,7 @@ public static void Main() var services = new ServiceCollection(); services.AddActors(options => { + options.UseJsonSerialization = true; options.Actors.RegisterActor(); }); } diff --git a/test/Dapr.Actors.Analyzers.Test/Utilities.cs b/test/Dapr.Actors.Analyzers.Test/Utilities.cs index dad3456c0..16dbfcbb0 100644 --- a/test/Dapr.Actors.Analyzers.Test/Utilities.cs +++ b/test/Dapr.Actors.Analyzers.Test/Utilities.cs @@ -32,7 +32,7 @@ internal static class Utilities var compilationWithAnalyzer = compilation.WithAnalyzers( ImmutableArray.Create( - new ActorRegistrationAnalyzer())); + new ActorAnalyzer())); // Get diagnostics from the compilation var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync(); diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs index ea45924dd..07f06403e 100644 --- a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs +++ b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs @@ -37,7 +37,7 @@ public static async Task VerifyAnalyzerAsync(string source, string? program, par test.TestState.Sources.Add(("Program.cs", program)); } - var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList(); + var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(ActorAnalyzer)).ToList(); metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); @@ -50,7 +50,7 @@ public static async Task VerifyAnalyzerAsync(string source, string? program, par await test.RunAsync(CancellationToken.None); } - private class Test : CSharpAnalyzerTest + private class Test : CSharpAnalyzerTest { public Test() { From 91bf38901c6614ca782f885d79a80960ba42ea35 Mon Sep 17 00:00:00 2001 From: Nils Gruson Date: Thu, 16 Jan 2025 10:07:31 +0100 Subject: [PATCH 3/4] Add DAPR0003 diagnostic for MapActorsHandlers usage Introduce a new diagnostic rule (DAPR0003) that warns users to call `app.MapActorsHandlers` for Dapr actors. Implemented in the `ActorAnalyzer` class with a corresponding code fix provider (`MapActorsHandlersCodeFixProvider`) to automatically add the call when needed. Updated `SupportedDiagnostics` to include DAPR0003 and added the `AnalyzeMapActorsHandlers` method for syntax tree analysis. Introduced `FindInvocation` to locate method calls in the syntax tree. Added tests in `ActorAnalyzerTests` and `MapActorsHandlersCodeFixProviderTests` to validate the new functionality, covering various scenarios. Updated `Utilities` and `VerifyAnalyzer` classes to include necessary assembly references for compatibility across .NET versions. Made minor adjustments to existing code and tests for consistency. Signed-off-by: Nils Gruson --- src/Dapr.Actors.Analyzers/ActorAnalyzer.cs | 128 ++++++----- .../ActorRegistrationCodeFixProvider.cs | 2 +- .../AnalyzerReleases.Shipped.md | 3 +- .../MapActorsHandlersCodeFixProvider.cs | 144 ++++++++++++ .../ActorAnalyzerTests.cs | 208 ++++++++++++------ ...orJsonSerializationCodeFixProviderTests.cs | 26 ++- .../ActorRegistrationCodeFixProviderTests.cs | 24 +- .../MapActorsHandlersCodeFixProviderTests.cs | 124 +++++++++++ test/Dapr.Actors.Analyzers.Test/Utilities.cs | 28 ++- .../VerifyAnalyzer.cs | 5 +- 10 files changed, 546 insertions(+), 146 deletions(-) create mode 100644 src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs diff --git a/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs index e3f03ac25..02edc7b3a 100644 --- a/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs +++ b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs @@ -28,11 +28,21 @@ public class ActorAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, isEnabledByDefault: true); + private static readonly DiagnosticDescriptor DiagnosticDescriptorMapActorsHandlers = new( + "DAPR0003", + "Call MapActorsHandlers", + "Call app.MapActorsHandlers to map endpoints for Dapr actors", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + /// /// Gets the supported diagnostics for this analyzer. /// public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - DiagnosticDescriptorActorRegistration, DiagnosticDescriptorJsonSerialization); + DiagnosticDescriptorActorRegistration, + DiagnosticDescriptorJsonSerialization, + DiagnosticDescriptorMapActorsHandlers); /// /// Initializes the analyzer. @@ -44,12 +54,13 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration); context.RegisterSyntaxNodeAction(AnalyzeSerialization, SyntaxKind.CompilationUnit); + context.RegisterSyntaxNodeAction(AnalyzeMapActorsHandlers, SyntaxKind.CompilationUnit); } - + private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context) { var classDeclaration = (ClassDeclarationSyntax)context.Node; - + if (classDeclaration.BaseList != null) { var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type; @@ -57,7 +68,7 @@ private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context) if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is INamedTypeSymbol baseTypeSymbol) { var baseTypeName = baseTypeSymbol.ToDisplayString(); - + { var actorTypeName = classDeclaration.Identifier.Text; bool isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel); @@ -70,7 +81,7 @@ private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context) } } } - + private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel semanticModel) { var methodInvocations = new List(); @@ -114,81 +125,82 @@ private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel } private void AnalyzeSerialization(SyntaxNodeAnalysisContext context) - { - var actorTypes = GetActorDerivedClasses(context); - InvocationExpressionSyntax? addActorsInvocation = null; + { + var addActorsInvocation = FindInvocation(context, "AddActors"); - if (actorTypes.Any()) + if (addActorsInvocation != null) { - foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees) + var optionsLambda = addActorsInvocation.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda != null) { - var root = syntaxTree.GetRoot(); - addActorsInvocation = root.DescendantNodes().OfType() - .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && - memberAccess.Name.Identifier.Text == "AddActors"); + var lambdaBody = optionsLambda.Body; + var assignments = lambdaBody.DescendantNodes().OfType(); + + var useJsonSerialization = assignments.Any(assignment => + assignment.Left is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "UseJsonSerialization" && + assignment.Right is LiteralExpressionSyntax literal && + literal.Token.ValueText == "true"); - if (addActorsInvocation != null) + if (!useJsonSerialization) { - break; + var diagnostic = Diagnostic.Create(DiagnosticDescriptorJsonSerialization, addActorsInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); } } + } + } - if (addActorsInvocation != null) - { - var optionsLambda = addActorsInvocation.ArgumentList.Arguments - .Select(arg => arg.Expression) - .OfType() - .FirstOrDefault(); - - if (optionsLambda != null) - { - var lambdaBody = optionsLambda.Body; - var assignments = lambdaBody.DescendantNodes().OfType(); - - var useJsonSerialization = assignments.Any(assignment => - assignment.Left is MemberAccessExpressionSyntax memberAccess && - memberAccess.Name is IdentifierNameSyntax identifier && - identifier.Identifier.Text == "UseJsonSerialization" && - assignment.Right is LiteralExpressionSyntax literal && - literal.Token.ValueText == "true"); + private InvocationExpressionSyntax? FindInvocation(SyntaxNodeAnalysisContext context, string methodName) + { + foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + var invocation = root.DescendantNodes().OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == methodName); - if (!useJsonSerialization) - { - var diagnostic = Diagnostic.Create(DiagnosticDescriptorJsonSerialization, addActorsInvocation.GetLocation()); - context.ReportDiagnostic(diagnostic); - } - } + if (invocation != null) + { + return invocation; } } + + return null; } - private static IEnumerable GetActorDerivedClasses(SyntaxNodeAnalysisContext context) + private void AnalyzeMapActorsHandlers(SyntaxNodeAnalysisContext context) { - var compilation = context.SemanticModel.Compilation; - var actorDerivedClasses = new List(); + var addActorsInvocation = FindInvocation(context, "AddActors"); - foreach (var syntaxTree in compilation.SyntaxTrees) - { - var root = syntaxTree.GetRoot(); - var classDeclarations = root.DescendantNodes().OfType(); + if (addActorsInvocation != null) + { + bool invokedByWebApplication = false; + var mapActorsHandlersInvocation = FindInvocation(context, "MapActorsHandlers"); - foreach (var classDeclaration in classDeclarations) + if (mapActorsHandlersInvocation?.Expression is MemberAccessExpressionSyntax memberAccess) { - if (classDeclaration.BaseList != null) + var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess.Expression); + if (symbolInfo.Symbol is ILocalSymbol localSymbol) { - var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type; - - if (baseTypeSyntax is IdentifierNameSyntax identifier) + var type = localSymbol.Type; + if (type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplication") { - if (identifier.Identifier.Text == "Dapr.Actors.Runtime.Actor" || identifier.Identifier.Text == "Actor") - { - actorDerivedClasses.Add(classDeclaration); - } + invokedByWebApplication = true; } } } - } - return actorDerivedClasses; + if (mapActorsHandlersInvocation == null || !invokedByWebApplication) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapActorsHandlers, addActorsInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } } } diff --git a/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs index d30d9f5ba..efb7d2994 100644 --- a/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs +++ b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs @@ -41,7 +41,7 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) private async Task RegisterActorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetSyntaxRootAsync(cancellationToken); var diagnosticSpan = diagnostic.Location.SourceSpan; var classDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); diff --git a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md index d377176c5..d8abdf2e2 100644 --- a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md @@ -5,4 +5,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- DAPR0001| Usage | Warning | The actor class '{0}' is not registered -DAPR0002| Usage | Warning | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors \ No newline at end of file +DAPR0002| Usage | Warning | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors +DAPR0003| Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs new file mode 100644 index 000000000..c79aebf6c --- /dev/null +++ b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs @@ -0,0 +1,144 @@ +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 Dapr.Actors.Analyzers; + +/// +/// Provides a code fix for the diagnostic "DAPR0003" by adding a call to MapActorsHandlers. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))] +[Shared] +public class MapActorsHandlersCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this code fix provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0003"); + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + /// Registers code fixes for the specified diagnostic. + /// + /// A context for code fix registration. + /// A task that represents the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = "Call MapActorsHandlers"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => AddMapActorsHandlersAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + /// + /// Adds a call to MapActorsHandlers to the specified document. + /// + /// The document to modify. + /// The diagnostic to fix. + /// A cancellation token. + /// A task that represents the asynchronous operation. The task result contains the modified document. + private async Task AddMapActorsHandlersAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken); + var invocationExpressions = root!.DescendantNodes().OfType(); + + if (invocationExpressions.Any()) + { + var createBuilderInvocation = invocationExpressions + .FirstOrDefault(invocation => + { + return invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "CreateBuilder" && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "WebApplication"; + }); + + if (createBuilderInvocation != null) + { + var variableDeclarator = createBuilderInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + if (variableDeclarator != null) + { + var variableName = variableDeclarator.Identifier.Text; + + var buildInvocation = invocationExpressions + .FirstOrDefault(invocation => + { + return invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "Build" && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == variableName; + }); + + if (buildInvocation != null) + { + var buildVariableDeclarator = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + if (buildVariableDeclarator != null) + { + var buildVariableName = buildVariableDeclarator.Identifier.Text; + + var mapActorsHandlersInvocation = SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(buildVariableName), + SyntaxFactory.IdentifierName("MapActorsHandlers")))); + + if (buildInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var localDeclaration = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, new[] { mapActorsHandlersInvocation }); + root = root.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var buildInvocationGlobalStatement = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement, + new[] { SyntaxFactory.GlobalStatement(mapActorsHandlersInvocation) }); + root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); + } + + return document.WithSyntaxRoot(root); + } + } + } + } + + return document; + } + + return document; + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs index ec5a2f533..8d6b9abed 100644 --- a/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs @@ -72,6 +72,26 @@ public async Task ReportNoDiagnostic() { var testCode = @" using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } class TestActor : Actor { @@ -81,31 +101,76 @@ public TestActor(ActorHost host) : base(host) } "; - var startupCode = @" + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task ReportNoDiagnostic_WithNamespace() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; - internal static class Extensions + public static class Program { - public static void AddApplicationServices(this IServiceCollection services) + public static void Main() { - services.AddActors(options => - { - options.Actors.RegisterActor(); + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); options.UseJsonSerialization = true; }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } } } "; - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, startupCode); + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); } [Fact] - public async Task ReportNoDiagnostic_WithNamespace() + public async Task ReportNoDiagnostic_WithNamespaceAlias () { var testCode = @" using Dapr.Actors.Runtime; - + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using alias = TestNamespace; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + namespace TestNamespace { class TestActor : Actor @@ -117,93 +182,101 @@ public TestActor(ActorHost host) : base(host) } "; - var startupCode = @" - using Microsoft.Extensions.DependencyInjection; + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + } + + public class JsonSerialization + { + [Fact] + public async Task ReportDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; - internal static class Extensions + public static class Program { - public static void AddApplicationServices(this IServiceCollection services) + public static void Main() { - services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { }); + + var app = builder.Build(); + + app.MapActorsHandlers(); } } "; - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, startupCode); + var expected = VerifyAnalyzer.Diagnostic("DAPR0002", DiagnosticSeverity.Warning) + .WithSpan(12, 25, 14, 27).WithMessage("Add options.UseJsonSerialization to support interoperability with non-.NET actors"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); } [Fact] - public async Task ReportNoDiagnostic_WithNamespaceAlias () + public async Task ReportNoDiagnostic() { var testCode = @" using Dapr.Actors.Runtime; - - namespace TestNamespace - { - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - } - "; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; - var startupCode = @" - using Microsoft.Extensions.DependencyInjection; - using alias = TestNamespace; - - internal static class Extensions + public static class Program { - public static void AddApplicationServices(this IServiceCollection services) + public static void Main() { - services.AddActors(options => + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => { - options.Actors.RegisterActor(); options.UseJsonSerialization = true; }); + + var app = builder.Build(); + + app.MapActorsHandlers(); } } "; - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, startupCode); + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); } } - - public class JsonSerialization + + public class MapActorsHandlers { [Fact] public async Task ReportDiagnostic() { var testCode = @" using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; - internal static class Extensions + public static class Program { - public static void AddApplicationServices(this IServiceCollection services) + public static void Main() { - services.AddActors(options => - { - options.Actors.RegisterActor(); + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; }); - } - } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { + + var app = builder.Build(); } } "; - var expected = VerifyAnalyzer.Diagnostic("DAPR0002", DiagnosticSeverity.Warning) - .WithSpan(9, 25, 12, 27).WithMessage("Add options.UseJsonSerialization to support interoperability with non-.NET actors"); + var expected = VerifyAnalyzer.Diagnostic("DAPR0003", DiagnosticSeverity.Warning) + .WithSpan(12, 25, 15, 27).WithMessage("Call app.MapActorsHandlers to map endpoints for Dapr actors"); await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); } @@ -213,27 +286,26 @@ public async Task ReportNoDiagnostic() { var testCode = @" using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; - internal static class Extensions + public static class Program { - public static void AddApplicationServices(this IServiceCollection services) + public static void Main() { - services.AddActors(options => - { - options.Actors.RegisterActor(); + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { options.UseJsonSerialization = true; }); + + var app = builder.Build(); + + app.MapActorsHandlers(); } } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; + "; await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); } diff --git a/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs index db8072ed8..7d3fe6cad 100644 --- a/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs @@ -6,34 +6,46 @@ public class ActorJsonSerializationCodeFixProviderTests public async Task UseJsonSerialization() { var code = @" - using Dapr.Actors.Runtime; + //using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; public static class Program { public static void Main() { - var services = new ServiceCollection(); - services.AddActors(options => + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => { }); + + var app = builder.Build(); + + app.MapActorsHandlers(); } } "; var expectedChangedCode = @" - using Dapr.Actors.Runtime; + //using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; - + public static class Program { public static void Main() { - var services = new ServiceCollection(); - services.AddActors(options => + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => { options.UseJsonSerialization = true; }); + + var app = builder.Build(); + + app.MapActorsHandlers(); } } "; diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs index 642dba40e..d148ed90b 100644 --- a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs @@ -7,17 +7,23 @@ public async Task RegisterActor() { var code = @" using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; public static class Program { public static void Main() { - var services = new ServiceCollection(); - services.AddActors(options => - { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { options.UseJsonSerialization = true; }); + + var app = builder.Build(); + + app.MapActorsHandlers(); } } @@ -31,18 +37,24 @@ public TestActor(ActorHost host) : base(host) var expectedChangedCode = @" using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; public static class Program { public static void Main() { - var services = new ServiceCollection(); - services.AddActors(options => - { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { options.UseJsonSerialization = true; options.Actors.RegisterActor(); }); + + var app = builder.Build(); + + app.MapActorsHandlers(); } } diff --git a/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs new file mode 100644 index 000000000..8d0f58350 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs @@ -0,0 +1,124 @@ +namespace Dapr.Actors.Analyzers.Test; + +public class MapActorsHandlersCodeFixProviderTests +{ + [Fact] + public async Task RegisterActor() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_TopLevelStatements() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/Utilities.cs b/test/Dapr.Actors.Analyzers.Test/Utilities.cs index 16dbfcbb0..4b6beed0a 100644 --- a/test/Dapr.Actors.Analyzers.Test/Utilities.cs +++ b/test/Dapr.Actors.Analyzers.Test/Utilities.cs @@ -4,7 +4,9 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis.Testing; namespace Dapr.Actors.Analyzers.Test; @@ -14,12 +16,29 @@ internal static class Utilities { var workspace = new AdhocWorkspace(); +#if NET6_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net60; +#elif NET7_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net70; +#elif NET8_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + // Create a new project with necessary references var project = workspace.AddProject("TestProject", LanguageNames.CSharp) - .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication) + .WithSpecificDiagnosticOptions(new Dictionary + { + { "CS1701", ReportDiagnostic.Suppress } + })) + .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default)) .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication))); + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsEndpointRouteBuilderExtensions))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); // Add the document to the project var document = project.AddDocument("TestDocument.cs", code); @@ -28,7 +47,8 @@ internal static class Utilities var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null"); var compilation = CSharpCompilation.Create("TestCompilation") .AddSyntaxTrees(syntaxTree) - .AddReferences(project.MetadataReferences); + .AddReferences(project.MetadataReferences) + .WithOptions(project.CompilationOptions!); var compilationWithAnalyzer = compilation.WithAnalyzers( ImmutableArray.Create( diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs index 07f06403e..d114caedf 100644 --- a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs +++ b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs @@ -1,8 +1,9 @@ using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis; -using Dapr.Actors.Runtime; using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; namespace Dapr.Actors.Analyzers.Test; @@ -40,6 +41,8 @@ public static async Task VerifyAnalyzerAsync(string source, string? program, par var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(ActorAnalyzer)).ToList(); metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); foreach (var reference in metadataReferences) { From 6ec8a1320e369b666cb89898fedc651a2939f6b0 Mon Sep 17 00:00:00 2001 From: Nils Gruson Date: Thu, 16 Jan 2025 17:01:30 +0100 Subject: [PATCH 4/4] Refactor MapActorsHandlersCodeFixProvider logic Simplify handling of createBuilderInvocation and buildInvocation by removing unnecessary nested checks. Improve code readability with clearer conditional blocks. Ensure the document is returned with the modified syntax root after changes. Signed-off-by: Nils Gruson --- .../MapActorsHandlersCodeFixProvider.cs | 139 ++++++++---------- 1 file changed, 60 insertions(+), 79 deletions(-) diff --git a/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs index c79aebf6c..60dc7a4e8 100644 --- a/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs +++ b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs @@ -58,87 +58,68 @@ private async Task AddMapActorsHandlersAsync(Document document, Diagno var root = await document.GetSyntaxRootAsync(cancellationToken); var invocationExpressions = root!.DescendantNodes().OfType(); - if (invocationExpressions.Any()) - { - var createBuilderInvocation = invocationExpressions - .FirstOrDefault(invocation => - { - return invocation.Expression is MemberAccessExpressionSyntax memberAccess && - memberAccess.Name.Identifier.Text == "CreateBuilder" && - memberAccess.Expression is IdentifierNameSyntax identifier && - identifier.Identifier.Text == "WebApplication"; - }); - - if (createBuilderInvocation != null) + var createBuilderInvocation = invocationExpressions + .FirstOrDefault(invocation => + { + return invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "CreateBuilder" && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "WebApplication"; + }); + + var variableDeclarator = createBuilderInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var variableName = variableDeclarator.Identifier.Text; + + var buildInvocation = invocationExpressions + .FirstOrDefault(invocation => { - var variableDeclarator = createBuilderInvocation - .AncestorsAndSelf() - .OfType() - .FirstOrDefault(); - - if (variableDeclarator != null) - { - var variableName = variableDeclarator.Identifier.Text; - - var buildInvocation = invocationExpressions - .FirstOrDefault(invocation => - { - return invocation.Expression is MemberAccessExpressionSyntax memberAccess && - memberAccess.Name.Identifier.Text == "Build" && - memberAccess.Expression is IdentifierNameSyntax identifier && - identifier.Identifier.Text == variableName; - }); - - if (buildInvocation != null) - { - var buildVariableDeclarator = buildInvocation - .AncestorsAndSelf() - .OfType() - .FirstOrDefault(); - - if (buildVariableDeclarator != null) - { - var buildVariableName = buildVariableDeclarator.Identifier.Text; - - var mapActorsHandlersInvocation = SyntaxFactory.ExpressionStatement( - SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName(buildVariableName), - SyntaxFactory.IdentifierName("MapActorsHandlers")))); - - if (buildInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) - { - var localDeclaration = buildInvocation - .AncestorsAndSelf() - .OfType() - .FirstOrDefault(); - - var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, new[] { mapActorsHandlersInvocation }); - root = root.ReplaceNode(parentBlock, newParentBlock); - } - else - { - var buildInvocationGlobalStatement = buildInvocation - .AncestorsAndSelf() - .OfType() - .FirstOrDefault(); - - var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); - var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement, - new[] { SyntaxFactory.GlobalStatement(mapActorsHandlersInvocation) }); - root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); - } - - return document.WithSyntaxRoot(root); - } - } - } - } - - return document; + return invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "Build" && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == variableName; + }); + + var buildVariableDeclarator = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var buildVariableName = buildVariableDeclarator.Identifier.Text; + + var mapActorsHandlersInvocation = SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(buildVariableName), + SyntaxFactory.IdentifierName("MapActorsHandlers")))); + + if (buildInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var localDeclaration = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, new[] { mapActorsHandlersInvocation }); + root = root.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var buildInvocationGlobalStatement = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement, + new[] { SyntaxFactory.GlobalStatement(mapActorsHandlersInvocation) }); + root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); } - return document; + return document.WithSyntaxRoot(root); } }