Skip to content

Commit

Permalink
Add analyzers and code fixes for Dapr workflows
Browse files Browse the repository at this point in the history
Introduce `WorkflowRegistrationAnalyzer` and `WorkflowActivityAnalyzer` to validate workflow and activity registrations in the DI container. Implement corresponding code fix providers to suggest automatic fixes. Add tests to ensure functionality and robustness of the Dapr workflow framework.
  • Loading branch information
ngruson committed Jan 9, 2025
1 parent 849baec commit 37db1c9
Show file tree
Hide file tree
Showing 12 changed files with 868 additions and 212 deletions.
3 changes: 2 additions & 1 deletion src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
DAPR1001| Usage | Warning | The class '{0}' is not registered
DAPR1001| Usage | Warning | The workflow class '{0}' is not registered
DAPR1002| Usage | Warning | The workflow activity class '{0}' is not registered
104 changes: 0 additions & 104 deletions src/Dapr.Workflow.Analyzers/WorkflowActivityAnalyzer.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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;
using Microsoft.CodeAnalysis.Formatting;

namespace Dapr.Workflow.Analyzers;

/// <summary>
/// Provides code fixes for DAPR1002 diagnostic.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(WorkflowActivityRegistrationCodeFixProvider))]
[Shared]
public class WorkflowActivityRegistrationCodeFixProvider : CodeFixProvider
{
/// <summary>
/// Gets the diagnostic IDs that this provider can fix.
/// </summary>
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create("DAPR1002");

/// <summary>
/// Registers the code fix for the diagnostic.
/// </summary>
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var title = "Register workflow activity";
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: c => RegisterWorkflowActivityAsync(context.Document, context.Diagnostics.First(), c),
equivalenceKey: title),
context.Diagnostics);
return Task.CompletedTask;
}

private async Task<Document> RegisterWorkflowActivityAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var diagnosticSpan = diagnostic.Location.SourceSpan;

var oldInvocation = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().First();

if (oldInvocation is null)
return document;

if (root == null || oldInvocation == null)
return document;

// Extract the workflow activity type name
var workflowActivityType = oldInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString();

if (string.IsNullOrEmpty(workflowActivityType))
return document;

// Get the compilation
var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);

if (compilation == null)
return document;

InvocationExpressionSyntax? addDaprWorkflowInvocation = null;
SyntaxNode? targetRoot = null;
Document? targetDocument = null;

// Iterate through all syntax trees in the compilation
foreach (var syntaxTree in compilation.SyntaxTrees)
{
var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);

addDaprWorkflowInvocation = syntaxRoot.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Name.Identifier.Text == "AddDaprWorkflow");

if (addDaprWorkflowInvocation != null)
{
targetRoot = syntaxRoot;
targetDocument = document.Project.GetDocument(syntaxTree);
break;
}
}

if (addDaprWorkflowInvocation == null || targetRoot == null || targetDocument == null)
return document;

// Find the options lambda block
var optionsLambda = addDaprWorkflowInvocation.ArgumentList.Arguments
.Select(arg => arg.Expression)
.OfType<SimpleLambdaExpressionSyntax>()
.FirstOrDefault();

// 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}.RegisterActivity<{workflowActivityType}>();");

if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock)
return document;

// 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);
}

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

0 comments on commit 37db1c9

Please sign in to comment.