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 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
<PackageVersion Include="Dapr.Actors" Version="1.15.0-rc01" />
<PackageVersion Include="FluentAssertions" Version="5.9.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="1.1.2" />
<PackageVersion Include="Google.Api.CommonProtos" Version="2.2.0" />
Expand All @@ -21,6 +22,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
Expand Down Expand Up @@ -49,4 +51,4 @@
<PackageVersion Include="xunit.extensibility.core" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
</Project>
</Project>
14 changes: 14 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
206 changes: 206 additions & 0 deletions src/Dapr.Actors.Analyzers/ActorAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Analyzes actor registration in Dapr applications.
/// </summary>
[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);

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

/// <summary>
/// Initializes the analyzer.
/// </summary>
/// <param name="context">The analysis context.</param>
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<InvocationExpressionSyntax>();
foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees)
{
var root = syntaxTree.GetRoot();
methodInvocations.AddRange(root.DescendantNodes().OfType<InvocationExpressionSyntax>());
}

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<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");

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<InvocationExpressionSyntax>()
.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);
}
}
}
}
120 changes: 120 additions & 0 deletions src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides code fix to enable JSON serialization for actors.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))]
[Shared]
public class ActorJsonSerializationCodeFixProvider : CodeFixProvider
{
/// <summary>
/// Gets the diagnostic IDs that this provider can fix.
/// </summary>
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create("DAPR0002");

/// <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 diagnostics.
/// </summary>
/// <param name="context">The context to register the code fixes.</param>
/// <returns>A task representing the asynchronous operation.</returns>
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<Document> 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<SimpleLambdaExpressionSyntax>()
.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<ExpressionStatementSyntax>()
.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<InvocationExpressionSyntax>()
.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);
}
}
Loading