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/ActorAnalyzer.cs b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs new file mode 100644 index 000000000..02edc7b3a --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs @@ -0,0 +1,206 @@ +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); + + 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, + DiagnosticDescriptorMapActorsHandlers); + + /// + /// 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); + 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; + + 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 addActorsInvocation = FindInvocation(context, "AddActors"); + + 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 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 (invocation != null) + { + return invocation; + } + } + + return null; + } + + private void AnalyzeMapActorsHandlers(SyntaxNodeAnalysisContext context) + { + var addActorsInvocation = FindInvocation(context, "AddActors"); + + if (addActorsInvocation != null) + { + bool invokedByWebApplication = false; + var mapActorsHandlersInvocation = FindInvocation(context, "MapActorsHandlers"); + + if (mapActorsHandlersInvocation?.Expression is MemberAccessExpressionSyntax memberAccess) + { + var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess.Expression); + if (symbolInfo.Symbol is ILocalSymbol localSymbol) + { + var type = localSymbol.Type; + if (type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplication") + { + invokedByWebApplication = true; + } + } + } + + if (mapActorsHandlersInvocation == null || !invokedByWebApplication) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapActorsHandlers, addActorsInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } +} 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/ActorRegistrationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs new file mode 100644 index 000000000..efb7d2994 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs @@ -0,0 +1,220 @@ +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; + } + + private async Task RegisterActorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken); + 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); + + 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..d8abdf2e2 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,9 @@ +## Release 1.16 + +### New Rules + +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 +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/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/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs new file mode 100644 index 000000000..60dc7a4e8 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs @@ -0,0 +1,125 @@ +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(); + + 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 => + { + 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.WithSyntaxRoot(root); + } +} 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/ActorAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs new file mode 100644 index 000000000..8d6b9abed --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs @@ -0,0 +1,313 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Analyzers.Test; + +public class ActorAnalyzerTests +{ + 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; + 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 VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task ReportNoDiagnostic_WithNamespace() + { + 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(); + } + } + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + 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 + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + 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; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + 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() + { + 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.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + } + + public class MapActorsHandlers + { + [Fact] + public async Task ReportDiagnostic() + { + 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.UseJsonSerialization = true; + }); + + var app = builder.Build(); + } + } + "; + + 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); + } + + [Fact] + 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.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + 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..7d3fe6cad --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs @@ -0,0 +1,55 @@ +namespace Dapr.Actors.Analyzers.Test; + +public class ActorJsonSerializationCodeFixProviderTests +{ + [Fact] + public async Task UseJsonSerialization() + { + 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 => + { + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + 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.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs new file mode 100644 index 000000000..d148ed90b --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs @@ -0,0 +1,175 @@ +namespace Dapr.Actors.Analyzers.Test; + +public class ActorRegistrationCodeFixProviderTests +{ + [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.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + 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.UseJsonSerialization = true; + options.Actors.RegisterActor(); + }); + + 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_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/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 new file mode 100644 index 000000000..4b6beed0a --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/Utilities.cs @@ -0,0 +1,77 @@ +using System.Collections.Immutable; +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp; +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; + +internal static class Utilities +{ + public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code) + { + 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.ConsoleApplication) + .WithSpecificDiagnosticOptions(new Dictionary + { + { "CS1701", ReportDiagnostic.Suppress } + })) + .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default)) + .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) + .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); + + // 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) + .WithOptions(project.CompilationOptions!); + + var compilationWithAnalyzer = compilation.WithAnalyzers( + ImmutableArray.Create( + new ActorAnalyzer())); + + // 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..d114caedf --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs @@ -0,0 +1,68 @@ +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; + +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(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) + { + 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); + } +}