Skip to content

Commit 76bfed2

Browse files
ngrusonWhitWaldo
andauthored
Add Roslyn analyzer for actor registration (#1441)
Added Roslyn analyzers and code fixes for: - Encouraging JSON serialization for actors - Validating actors are properly registered in DI - Ensuring that the actor handler mapping is performed during startup Signed-off-by: Nils Gruson <[email protected]> Signed-off-by: Whit Waldo <[email protected]> Co-authored-by: Whit Waldo <[email protected]>
1 parent e477cf7 commit 76bfed2

40 files changed

+3505
-17
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
2626
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.2" />
2727
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" />
28+
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.13.0" />
2829
<PackageVersion Include="Microsoft.DurableTask.Client.Grpc" Version="1.5.0" />
2930
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.5.0" />
3031
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace Dapr.Actors.Analyzers;
8+
9+
/// <summary>
10+
/// Analyzes actor registration in Dapr applications.
11+
/// </summary>
12+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
13+
public class ActorAnalyzer : DiagnosticAnalyzer
14+
{
15+
private static readonly DiagnosticDescriptor DiagnosticDescriptorActorRegistration = new(
16+
"DAPR0001",
17+
"Actor class not registered",
18+
"The actor class '{0}' is not registered",
19+
"Usage",
20+
DiagnosticSeverity.Warning,
21+
isEnabledByDefault: true);
22+
23+
private static readonly DiagnosticDescriptor DiagnosticDescriptorJsonSerialization = new(
24+
"DAPR0002",
25+
"Use JsonSerialization",
26+
"Add options.UseJsonSerialization to support interoperability with non-.NET actors",
27+
"Usage",
28+
DiagnosticSeverity.Warning,
29+
isEnabledByDefault: true);
30+
31+
private static readonly DiagnosticDescriptor DiagnosticDescriptorMapActorsHandlers = new(
32+
"DAPR0003",
33+
"Call MapActorsHandlers",
34+
"Call app.MapActorsHandlers to map endpoints for Dapr actors",
35+
"Usage",
36+
DiagnosticSeverity.Warning,
37+
isEnabledByDefault: true);
38+
39+
/// <summary>
40+
/// Gets the supported diagnostics for this analyzer.
41+
/// </summary>
42+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
43+
DiagnosticDescriptorActorRegistration,
44+
DiagnosticDescriptorJsonSerialization,
45+
DiagnosticDescriptorMapActorsHandlers);
46+
47+
/// <summary>
48+
/// Initializes the analyzer.
49+
/// </summary>
50+
/// <param name="context">The analysis context.</param>
51+
public override void Initialize(AnalysisContext context)
52+
{
53+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
54+
context.EnableConcurrentExecution();
55+
context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration);
56+
context.RegisterSyntaxNodeAction(AnalyzeSerialization, SyntaxKind.CompilationUnit);
57+
context.RegisterSyntaxNodeAction(AnalyzeMapActorsHandlers, SyntaxKind.CompilationUnit);
58+
}
59+
60+
private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context)
61+
{
62+
var classDeclaration = (ClassDeclarationSyntax)context.Node;
63+
64+
if (classDeclaration.BaseList != null)
65+
{
66+
var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type;
67+
68+
if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is INamedTypeSymbol baseTypeSymbol)
69+
{
70+
var baseTypeName = baseTypeSymbol.ToDisplayString();
71+
72+
{
73+
var actorTypeName = classDeclaration.Identifier.Text;
74+
bool isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel);
75+
if (!isRegistered)
76+
{
77+
var diagnostic = Diagnostic.Create(DiagnosticDescriptorActorRegistration, classDeclaration.Identifier.GetLocation(), actorTypeName);
78+
context.ReportDiagnostic(diagnostic);
79+
}
80+
}
81+
}
82+
}
83+
}
84+
85+
private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel semanticModel)
86+
{
87+
var methodInvocations = new List<InvocationExpressionSyntax>();
88+
foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees)
89+
{
90+
var root = syntaxTree.GetRoot();
91+
methodInvocations.AddRange(root.DescendantNodes().OfType<InvocationExpressionSyntax>());
92+
}
93+
94+
foreach (var invocation in methodInvocations)
95+
{
96+
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
97+
{
98+
continue;
99+
}
100+
101+
var methodName = memberAccess.Name.Identifier.Text;
102+
if (methodName == "RegisterActor")
103+
{
104+
if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0)
105+
{
106+
if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument)
107+
{
108+
if (typeArgument.Identifier.Text == actorTypeName)
109+
{
110+
return true;
111+
}
112+
}
113+
else if (typeArgumentList.TypeArgumentList.Arguments[0] is QualifiedNameSyntax qualifiedName)
114+
{
115+
if (qualifiedName.Right.Identifier.Text == actorTypeName)
116+
{
117+
return true;
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
return false;
125+
}
126+
127+
private void AnalyzeSerialization(SyntaxNodeAnalysisContext context)
128+
{
129+
var addActorsInvocation = FindInvocation(context, "AddActors");
130+
131+
if (addActorsInvocation != null)
132+
{
133+
var optionsLambda = addActorsInvocation.ArgumentList.Arguments
134+
.Select(arg => arg.Expression)
135+
.OfType<SimpleLambdaExpressionSyntax>()
136+
.FirstOrDefault();
137+
138+
if (optionsLambda != null)
139+
{
140+
var lambdaBody = optionsLambda.Body;
141+
var assignments = lambdaBody.DescendantNodes().OfType<AssignmentExpressionSyntax>();
142+
143+
var useJsonSerialization = assignments.Any(assignment =>
144+
assignment.Left is MemberAccessExpressionSyntax memberAccess &&
145+
memberAccess.Name is IdentifierNameSyntax identifier &&
146+
identifier.Identifier.Text == "UseJsonSerialization" &&
147+
assignment.Right is LiteralExpressionSyntax literal &&
148+
literal.Token.ValueText == "true");
149+
150+
if (!useJsonSerialization)
151+
{
152+
var diagnostic = Diagnostic.Create(DiagnosticDescriptorJsonSerialization, addActorsInvocation.GetLocation());
153+
context.ReportDiagnostic(diagnostic);
154+
}
155+
}
156+
}
157+
}
158+
159+
private InvocationExpressionSyntax? FindInvocation(SyntaxNodeAnalysisContext context, string methodName)
160+
{
161+
foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees)
162+
{
163+
var root = syntaxTree.GetRoot();
164+
var invocation = root.DescendantNodes().OfType<InvocationExpressionSyntax>()
165+
.FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
166+
memberAccess.Name.Identifier.Text == methodName);
167+
168+
if (invocation != null)
169+
{
170+
return invocation;
171+
}
172+
}
173+
174+
return null;
175+
}
176+
177+
private void AnalyzeMapActorsHandlers(SyntaxNodeAnalysisContext context)
178+
{
179+
var addActorsInvocation = FindInvocation(context, "AddActors");
180+
181+
if (addActorsInvocation != null)
182+
{
183+
bool invokedByWebApplication = false;
184+
var mapActorsHandlersInvocation = FindInvocation(context, "MapActorsHandlers");
185+
186+
if (mapActorsHandlersInvocation?.Expression is MemberAccessExpressionSyntax memberAccess)
187+
{
188+
var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess.Expression);
189+
if (symbolInfo.Symbol is ILocalSymbol localSymbol)
190+
{
191+
var type = localSymbol.Type;
192+
if (type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplication")
193+
{
194+
invokedByWebApplication = true;
195+
}
196+
}
197+
}
198+
199+
if (mapActorsHandlersInvocation == null || !invokedByWebApplication)
200+
{
201+
var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapActorsHandlers, addActorsInvocation.GetLocation());
202+
context.ReportDiagnostic(diagnostic);
203+
}
204+
}
205+
}
206+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using System.Composition;
2+
using Microsoft.CodeAnalysis.CodeFixes;
3+
using Microsoft.CodeAnalysis;
4+
using System.Collections.Immutable;
5+
using Microsoft.CodeAnalysis.CodeActions;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
9+
namespace Dapr.Actors.Analyzers;
10+
11+
/// <summary>
12+
/// Provides code fix to enable JSON serialization for actors.
13+
/// </summary>
14+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))]
15+
[Shared]
16+
public class ActorJsonSerializationCodeFixProvider : CodeFixProvider
17+
{
18+
/// <summary>
19+
/// Gets the diagnostic IDs that this provider can fix.
20+
/// </summary>
21+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create("DAPR0002");
22+
23+
/// <summary>
24+
/// Gets the FixAllProvider for this code fix provider.
25+
/// </summary>
26+
/// <returns>The FixAllProvider.</returns>
27+
public override FixAllProvider? GetFixAllProvider()
28+
{
29+
return WellKnownFixAllProviders.BatchFixer;
30+
}
31+
32+
/// <summary>
33+
/// Registers code fixes for the specified diagnostics.
34+
/// </summary>
35+
/// <param name="context">The context to register the code fixes.</param>
36+
/// <returns>A task representing the asynchronous operation.</returns>
37+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
38+
{
39+
var title = "Use JSON serialization";
40+
context.RegisterCodeFix(
41+
CodeAction.Create(
42+
title,
43+
createChangedDocument: c => UseJsonSerializationAsync(context.Document, context.Diagnostics.First(), c),
44+
equivalenceKey: title),
45+
context.Diagnostics);
46+
return Task.CompletedTask;
47+
}
48+
49+
private async Task<Document> UseJsonSerializationAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
50+
{
51+
(_, var addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken);
52+
53+
if (addActorsInvocation == null)
54+
{
55+
return document;
56+
}
57+
58+
var optionsLambda = addActorsInvocation?.ArgumentList.Arguments
59+
.Select(arg => arg.Expression)
60+
.OfType<SimpleLambdaExpressionSyntax>()
61+
.FirstOrDefault();
62+
63+
if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock)
64+
return document;
65+
66+
// Extract the parameter name from the lambda expression
67+
var parameterName = optionsLambda.Parameter.Identifier.Text;
68+
69+
// Check if the lambda body already contains the assignment
70+
var assignmentExists = optionsBlock.Statements
71+
.OfType<ExpressionStatementSyntax>()
72+
.Any(statement => statement.Expression is AssignmentExpressionSyntax assignment &&
73+
assignment.Left is MemberAccessExpressionSyntax memberAccess &&
74+
memberAccess.Name is IdentifierNameSyntax identifier &&
75+
identifier.Identifier.Text == parameterName &&
76+
memberAccess.Name.Identifier.Text == "UseJsonSerialization");
77+
78+
if (!assignmentExists)
79+
{
80+
var assignmentStatement = SyntaxFactory.ExpressionStatement(
81+
SyntaxFactory.AssignmentExpression(
82+
SyntaxKind.SimpleAssignmentExpression,
83+
SyntaxFactory.MemberAccessExpression(
84+
SyntaxKind.SimpleMemberAccessExpression,
85+
SyntaxFactory.IdentifierName(parameterName),
86+
SyntaxFactory.IdentifierName("UseJsonSerialization")),
87+
SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression)));
88+
89+
var newOptionsBlock = optionsBlock.AddStatements(assignmentStatement);
90+
var root = await document.GetSyntaxRootAsync(cancellationToken);
91+
var newRoot = root?.ReplaceNode(optionsBlock, newOptionsBlock);
92+
return document.WithSyntaxRoot(newRoot!);
93+
}
94+
95+
return document;
96+
}
97+
98+
private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken)
99+
{
100+
var compilation = await project.GetCompilationAsync(cancellationToken);
101+
102+
foreach (var syntaxTree in compilation!.SyntaxTrees)
103+
{
104+
var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken);
105+
106+
var addActorsInvocation = syntaxRoot.DescendantNodes()
107+
.OfType<InvocationExpressionSyntax>()
108+
.FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
109+
memberAccess.Name.Identifier.Text == "AddActors");
110+
111+
if (addActorsInvocation != null)
112+
{
113+
var document = project.GetDocument(addActorsInvocation.SyntaxTree);
114+
return (document, addActorsInvocation);
115+
}
116+
}
117+
118+
return (null, null);
119+
}
120+
}

0 commit comments

Comments
 (0)