Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
33 changes: 32 additions & 1 deletion src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ namespace Dapr.Workflow.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class WorkflowRegistrationAnalyzer : DiagnosticAnalyzer
{
private const string WorkflowVersioningExtensionsMetadataName =
"Dapr.Workflow.Versioning.WorkflowVersioningServiceCollectionExtensions";

internal static readonly DiagnosticDescriptor WorkflowDiagnosticDescriptor = new(
id: "DAPR1301",
title: new LocalizableResourceString(nameof(Resources.DAPR1301Title), Resources.ResourceManager, typeof(Resources)),
Expand All @@ -33,7 +36,13 @@ public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeWorkflowRegistration, SyntaxKind.InvocationExpression);
context.RegisterCompilationStartAction(compilationContext =>
{
if (CheckIfWorkflowVersioningIsRegistered(compilationContext.Compilation))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While workflow versioning will automatically pick up all the workflows, I would also like for the analyzer to still run in case the user happens to define their own Workflow DI registrations, so can you ensure this scenario is covered as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a80f353. The deferred-diagnostics pattern keeps the per-workflow CheckIfWorkflowIsRegistered logic fully active even when versioning is present. For every ScheduleNewWorkflowAsync(nameof(X)) call, a node action checks explicit RegisterWorkflow<X> registrations first — if found, no diagnostic is added. If not found, a pending diagnostic is collected and later suppressed only because AddDaprWorkflowVersioning was confirmed. I also added a test VerifyWorkflowRegisteredWithVersioningPresent covering the combination of versioning + explicit registration.

return;

compilationContext.RegisterSyntaxNodeAction(AnalyzeWorkflowRegistration, SyntaxKind.InvocationExpression);
});
}

private static void AnalyzeWorkflowRegistration(SyntaxNodeAnalysisContext context)
Expand Down Expand Up @@ -70,6 +79,28 @@ private static void AnalyzeWorkflowRegistration(SyntaxNodeAnalysisContext contex
context.ReportDiagnostic(diagnostic);
}

private static bool CheckIfWorkflowVersioningIsRegistered(Compilation compilation)
{
var versioningExtensionsType = compilation.GetTypeByMetadataName(WorkflowVersioningExtensionsMetadataName);
if (versioningExtensionsType is null)
return false;

foreach (var syntaxTree in compilation.SyntaxTrees)
{
var root = syntaxTree.GetRoot();
foreach (var invocation in root.DescendantNodes().OfType<InvocationExpressionSyntax>())
{
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Name.Identifier.Text == "AddDaprWorkflowVersioning")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While a minor point, I would like for this to look for the AddDaprWorkflowVersioning method from the Dapr.Workflow.Versioning assembly, not just any at all (though we're unlikely to have such a conflict).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a80f353. The check now uses the deferred-diagnostics pattern to get a proper semantic model: when the versioning type is present in the compilation, a RegisterSyntaxNodeAction handler fires on every InvocationExpression and calls semanticModel.GetSymbolInfo(invocation) to verify the resolved method's ContainingType equals WorkflowVersioningServiceCollectionExtensions. Only if that exact Dapr method is confirmed does the RegisterCompilationEndAction suppress the collected diagnostics.

{
return true;
}
}
}

return false;
}

private static bool CheckIfWorkflowIsRegistered(INamedTypeSymbol workflowType, SemanticModel semanticModel, CancellationToken cancellationToken)
{
var methodInvocations = new List<InvocationExpressionSyntax>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Dapr.Workflow.Analyzers\Dapr.Workflow.Analyzers.csproj" />
<ProjectReference Include="..\..\src\Dapr.Workflow\Dapr.Workflow.csproj" />
<ProjectReference Include="..\..\src\Dapr.Workflow.Versioning.Abstractions\Dapr.Workflow.Versioning.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Dapr.Workflow.Versioning.Runtime\Dapr.Workflow.Versioning.Runtime.csproj" />
<ProjectReference Include="..\Dapr.Analyzers.Common\Dapr.Analyzers.Common.csproj" />
</ItemGroup>

Expand Down
2 changes: 2 additions & 0 deletions test/Dapr.Workflow.Analyzers.Test/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Immutable;
using Dapr.Analyzers.Common;
using Dapr.Common;
using Dapr.Workflow.Versioning;
using Microsoft.Extensions.Hosting;

namespace Dapr.Workflow.Analyzers.Test;
Expand All @@ -25,6 +26,7 @@ internal static IReadOnlyList<MetadataReference> GetReferences()
metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(Workflow<,>)));
metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowActivity<,>)));
metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(IDaprClient)));
metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowVersioningServiceCollectionExtensions)));
metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Task).Assembly.Location));
metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprWorkflowClient).Assembly.Location));
metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,50 @@ record OrderResult(string message) { }
await analyzer.VerifyAnalyzerAsync<WorkflowRegistrationAnalyzer>(testCode, expected);
}

[Fact]
public async Task VerifyWorkflowNotRegisteredButVersioningPresent()
{
const string testCode = """
using Dapr.Workflow;
using System.Threading.Tasks;

class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
return new OrderResult("Order processed");
}
}

class UseWorkflow()
{
public async Task RunWorkflow(DaprWorkflowClient client, OrderPayload order)
{
await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, order);
}
}

record OrderPayload { }
record OrderResult(string message) { }
""";

const string startupCode = """
using Dapr.Workflow.Versioning;
using Microsoft.Extensions.DependencyInjection;

internal static class Extensions
{
public static void AddApplicationServices(this IServiceCollection services)
{
services.AddDaprWorkflowVersioning();
}
}
""";

var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
await analyzer.VerifyAnalyzerAsync<WorkflowRegistrationAnalyzer>(testCode, startupCode);
}

[Fact]
public async Task VerifyWorkflowRegistered()
{
Expand Down
Loading