Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add Roslyn analyzer for actor registration #1441

Open
wants to merge 9 commits into
base: release-1.16
Choose a base branch
from
Prev Previous commit
Next Next commit
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 <ngruson@hotmail.com>
ngruson committed Jan 16, 2025
commit 91bf38901c6614ca782f885d79a80960ba42ea35
128 changes: 70 additions & 58 deletions src/Dapr.Actors.Analyzers/ActorAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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);

/// <summary>
/// Gets the supported diagnostics for this analyzer.
/// </summary>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
DiagnosticDescriptorActorRegistration, DiagnosticDescriptorJsonSerialization);
DiagnosticDescriptorActorRegistration,
DiagnosticDescriptorJsonSerialization,
DiagnosticDescriptorMapActorsHandlers);

/// <summary>
/// Initializes the analyzer.
@@ -44,20 +54,21 @@ 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;

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<InvocationExpressionSyntax>();
@@ -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<SimpleLambdaExpressionSyntax>()
.FirstOrDefault();

if (optionsLambda != null)
{
var root = syntaxTree.GetRoot();
addActorsInvocation = root.DescendantNodes().OfType<InvocationExpressionSyntax>()
.FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Name.Identifier.Text == "AddActors");
var lambdaBody = optionsLambda.Body;
var assignments = lambdaBody.DescendantNodes().OfType<AssignmentExpressionSyntax>();

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<SimpleLambdaExpressionSyntax>()
.FirstOrDefault();

if (optionsLambda != null)
{
var lambdaBody = optionsLambda.Body;
var assignments = lambdaBody.DescendantNodes().OfType<AssignmentExpressionSyntax>();

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<InvocationExpressionSyntax>()
.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<ClassDeclarationSyntax> GetActorDerivedClasses(SyntaxNodeAnalysisContext context)
private void AnalyzeMapActorsHandlers(SyntaxNodeAnalysisContext context)
{
var compilation = context.SemanticModel.Compilation;
var actorDerivedClasses = new List<ClassDeclarationSyntax>();
var addActorsInvocation = FindInvocation(context, "AddActors");

foreach (var syntaxTree in compilation.SyntaxTrees)
{
var root = syntaxTree.GetRoot();
var classDeclarations = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)

private async Task<Document> 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<ClassDeclarationSyntax>().First();
3 changes: 2 additions & 1 deletion src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -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
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
144 changes: 144 additions & 0 deletions src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides a code fix for the diagnostic "DAPR0003" by adding a call to MapActorsHandlers.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))]
[Shared]
public class MapActorsHandlersCodeFixProvider : CodeFixProvider
{
/// <summary>
/// Gets the diagnostic IDs that this code fix provider can fix.
/// </summary>
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create("DAPR0003");

/// <summary>
/// Gets the FixAllProvider for this code fix provider.
/// </summary>
/// <returns>The FixAllProvider.</returns>
public override FixAllProvider? GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

/// <summary>
/// Registers code fixes for the specified diagnostic.
/// </summary>
/// <param name="context">A context for code fix registration.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
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;
}

/// <summary>
/// Adds a call to MapActorsHandlers to the specified document.
/// </summary>
/// <param name="document">The document to modify.</param>
/// <param name="diagnostic">The diagnostic to fix.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the modified document.</returns>
private async Task<Document> AddMapActorsHandlersAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken);
var invocationExpressions = root!.DescendantNodes().OfType<InvocationExpressionSyntax>();

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<VariableDeclaratorSyntax>()
.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<VariableDeclaratorSyntax>()
.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<MethodDeclarationSyntax>().FirstOrDefault() is SyntaxNode parentBlock)
{
var localDeclaration = buildInvocation
.AncestorsAndSelf()
.OfType<LocalDeclarationStatementSyntax>()
.FirstOrDefault();

var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, new[] { mapActorsHandlersInvocation });
root = root.ReplaceNode(parentBlock, newParentBlock);
}
else
{
var buildInvocationGlobalStatement = buildInvocation
.AncestorsAndSelf()
.OfType<GlobalStatementSyntax>()
.FirstOrDefault();

var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType<CompilationUnitSyntax>().FirstOrDefault();
var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement,
new[] { SyntaxFactory.GlobalStatement(mapActorsHandlersInvocation) });
root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax);
}

return document.WithSyntaxRoot(root);
}
}
}
}

return document;
}

return document;
}
}
Loading