diff --git a/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md
index a69e0089..8a4a7a2b 100644
--- a/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md
+++ b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md
@@ -4,4 +4,5 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
-DAPR1001| Usage | Warning | The class '{0}' is not registered
\ No newline at end of file
+DAPR1001| Usage | Warning | The workflow class '{0}' is not registered
+DAPR1002| Usage | Warning | The workflow activity class '{0}' is not registered
\ No newline at end of file
diff --git a/src/Dapr.Workflow.Analyzers/WorkflowActivityAnalyzer.cs b/src/Dapr.Workflow.Analyzers/WorkflowActivityAnalyzer.cs
deleted file mode 100644
index 1ee6b556..00000000
--- a/src/Dapr.Workflow.Analyzers/WorkflowActivityAnalyzer.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using Microsoft.CodeAnalysis.Diagnostics;
-using Microsoft.CodeAnalysis;
-using System.Collections.Immutable;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-namespace Dapr.Workflow.Analyzers;
-
-///
-/// Analyzes whether or not workflow activities are registered.
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public class WorkflowActivityAnalyzer : DiagnosticAnalyzer
-{
- private static readonly DiagnosticDescriptor DiagnosticDescriptor = new(
- "DAPR1001",
- "Class not registered in DI",
- "The class '{0}' is not registered in the DI container",
- "Usage",
- DiagnosticSeverity.Warning,
- isEnabledByDefault: true);
-
-
- ///
- /// Gets the supported diagnostics for this analyzer.
- ///
- public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptor);
-
- ///
- /// Initializes the analyzer.
- ///
- /// The analysis context.
- public override void Initialize(AnalysisContext context)
- {
- context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
- context.EnableConcurrentExecution();
- context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression);
- }
-
- private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
- {
- var invocationExpr = (InvocationExpressionSyntax)context.Node;
-
- if (invocationExpr.Expression is not MemberAccessExpressionSyntax memberAccessExpr)
- return;
-
- if (memberAccessExpr.Name.Identifier.Text != "CallActivityAsync")
- return;
-
- var argumentList = invocationExpr.ArgumentList.Arguments;
- if (argumentList.Count == 0)
- return;
-
- var firstArgument = argumentList[0].Expression;
- if (firstArgument is InvocationExpressionSyntax nameofInvocation)
- {
- var activityName = nameofInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString().Trim('"');
- if (activityName != null)
- {
- bool isRegistered = CheckIfActivityIsRegistered(activityName, context.SemanticModel);
- if (!isRegistered)
- {
- var diagnostic = Diagnostic.Create(DiagnosticDescriptor, firstArgument.GetLocation(), activityName);
- context.ReportDiagnostic(diagnostic);
- }
- }
- }
- }
-
- private static bool CheckIfActivityIsRegistered(string activityName, SemanticModel semanticModel)
- {
- var methodInvocations = new List();
- foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees)
- {
- var root = syntaxTree.GetRoot();
- methodInvocations.AddRange(root.DescendantNodes().OfType());
- }
-
- foreach (var invocation in methodInvocations)
- {
- if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
- {
- continue;
- }
-
- var methodName = memberAccess.Name.Identifier.Text;
- if (methodName == "RegisterActivity")
- {
- if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0)
- {
- if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument)
- {
- if (string.Equals(typeArgument.Identifier.Text, activityName))
- {
- return true;
- }
- }
- }
- }
- }
-
- return false;
- }
-}
diff --git a/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationCodeFixProvider.cs b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationCodeFixProvider.cs
new file mode 100644
index 00000000..91158bb6
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationCodeFixProvider.cs
@@ -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;
+
+///
+/// Provides code fixes for DAPR1002 diagnostic.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(WorkflowActivityRegistrationCodeFixProvider))]
+[Shared]
+public class WorkflowActivityRegistrationCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR1002");
+
+ ///
+ /// Registers the code fix for the diagnostic.
+ ///
+ 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 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().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()
+ .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()
+ .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);
+ }
+
+ ///
+ /// Gets the FixAllProvider for this code fix provider.
+ ///
+ /// The FixAllProvider instance.
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+}
diff --git a/src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs
new file mode 100644
index 00000000..f335d717
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs
@@ -0,0 +1,178 @@
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Dapr.Workflow.Analyzers;
+
+///
+/// Analyzes whether or not workflow activities are registered.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class WorkflowRegistrationAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor WorkflowDiagnosticDescriptor = new(
+ "DAPR1001",
+ "Workflow not registered",
+ "The workflow class '{0}' is not registered",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor WorkflowActivityDiagnosticDescriptor = new(
+ "DAPR1002",
+ "Workflow activity not registered",
+ "The workflow activity class '{0}' is not registered",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+
+ ///
+ /// Gets the supported diagnostics for this analyzer.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(WorkflowDiagnosticDescriptor, WorkflowActivityDiagnosticDescriptor);
+
+ ///
+ /// Initializes the analyzer.
+ ///
+ /// The analysis context.
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeWorkflowRegistration, SyntaxKind.InvocationExpression);
+ context.RegisterSyntaxNodeAction(AnalyzeWorkflowActivityRegistration, SyntaxKind.InvocationExpression);
+ }
+
+ private void AnalyzeWorkflowRegistration(SyntaxNodeAnalysisContext context)
+ {
+ var invocationExpr = (InvocationExpressionSyntax)context.Node;
+
+ if (invocationExpr.Expression is not MemberAccessExpressionSyntax memberAccessExpr)
+ return;
+
+ if (memberAccessExpr.Name.Identifier.Text != "ScheduleNewWorkflowAsync")
+ return;
+
+ var argumentList = invocationExpr.ArgumentList.Arguments;
+ if (argumentList.Count == 0)
+ return;
+
+ var firstArgument = argumentList[0].Expression;
+ if (firstArgument is InvocationExpressionSyntax nameofInvocation)
+ {
+ var workflowName = nameofInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString().Trim('"');
+ if (workflowName != null)
+ {
+ bool isRegistered = CheckIfWorkflowIsRegistered(workflowName, context.SemanticModel);
+ if (!isRegistered)
+ {
+ var diagnostic = Diagnostic.Create(WorkflowDiagnosticDescriptor, firstArgument.GetLocation(), workflowName);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+ }
+
+ private void AnalyzeWorkflowActivityRegistration(SyntaxNodeAnalysisContext context)
+ {
+ var invocationExpr = (InvocationExpressionSyntax)context.Node;
+
+ if (invocationExpr.Expression is not MemberAccessExpressionSyntax memberAccessExpr)
+ return;
+
+ if (memberAccessExpr.Name.Identifier.Text != "CallActivityAsync")
+ return;
+
+ var argumentList = invocationExpr.ArgumentList.Arguments;
+ if (argumentList.Count == 0)
+ return;
+
+ var firstArgument = argumentList[0].Expression;
+ if (firstArgument is InvocationExpressionSyntax nameofInvocation)
+ {
+ var activityName = nameofInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString().Trim('"');
+ if (activityName != null)
+ {
+ bool isRegistered = CheckIfActivityIsRegistered(activityName, context.SemanticModel);
+ if (!isRegistered)
+ {
+ var diagnostic = Diagnostic.Create(WorkflowActivityDiagnosticDescriptor, firstArgument.GetLocation(), activityName);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+ }
+
+ private static bool CheckIfWorkflowIsRegistered(string workflowName, SemanticModel semanticModel)
+ {
+ var methodInvocations = new List();
+ foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees)
+ {
+ var root = syntaxTree.GetRoot();
+ methodInvocations.AddRange(root.DescendantNodes().OfType());
+ }
+
+ foreach (var invocation in methodInvocations)
+ {
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ {
+ continue;
+ }
+
+ var methodName = memberAccess.Name.Identifier.Text;
+ if (methodName == "RegisterWorkflow")
+ {
+ if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0)
+ {
+ if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument)
+ {
+ if (typeArgument.Identifier.Text == workflowName)
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static bool CheckIfActivityIsRegistered(string activityName, SemanticModel semanticModel)
+ {
+ var methodInvocations = new List();
+ foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees)
+ {
+ var root = syntaxTree.GetRoot();
+ methodInvocations.AddRange(root.DescendantNodes().OfType());
+ }
+
+ foreach (var invocation in methodInvocations)
+ {
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ {
+ continue;
+ }
+
+ var methodName = memberAccess.Name.Identifier.Text;
+ if (methodName == "RegisterActivity")
+ {
+ if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0)
+ {
+ if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument)
+ {
+ if (typeArgument.Identifier.Text == activityName)
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Dapr.Workflow.Analyzers/WorkflowRegistrationCodeFixProvider.cs b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationCodeFixProvider.cs
new file mode 100644
index 00000000..06e34e36
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationCodeFixProvider.cs
@@ -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;
+
+///
+/// Provides code fixes for DAPR1001 diagnostic.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(WorkflowRegistrationCodeFixProvider))]
+[Shared]
+public class WorkflowRegistrationCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR1001");
+
+ ///
+ /// Registers the code fix for the diagnostic.
+ ///
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var title = "Register workflow";
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title,
+ createChangedDocument: c => RegisterWorkflowAsync(context.Document, context.Diagnostics.First(), c),
+ equivalenceKey: title),
+ context.Diagnostics);
+ return Task.CompletedTask;
+ }
+
+ private async Task RegisterWorkflowAsync(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().First();
+
+ if (oldInvocation is null)
+ return document;
+
+ if (root == null || oldInvocation == null)
+ return document;
+
+ // Extract the workflow type name
+ var workflowType = oldInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString();
+
+ if (string.IsNullOrEmpty(workflowType))
+ 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()
+ .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()
+ .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;
+
+ // Create the new workflow registration statement
+ var registerWorkflowStatement = SyntaxFactory.ParseStatement($"{parameterName}.RegisterWorkflow<{workflowType}>();");
+
+ // 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);
+ }
+
+ ///
+ /// Gets the FixAllProvider for this code fix provider.
+ ///
+ /// The FixAllProvider instance.
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+}
diff --git a/test/Dapr.Workflow.Analyzers.Test/Utilities.cs b/test/Dapr.Workflow.Analyzers.Test/Utilities.cs
new file mode 100644
index 00000000..51ebfef4
--- /dev/null
+++ b/test/Dapr.Workflow.Analyzers.Test/Utilities.cs
@@ -0,0 +1,54 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.Reflection;
+
+namespace Dapr.Workflow.Analyzers.Test;
+
+internal static class Utilities
+{
+ public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code)
+ {
+ var workspace = new AdhocWorkspace();
+
+ // Create a new project with necessary references
+ var project = workspace.AddProject("TestProject", LanguageNames.CSharp)
+ .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
+ .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
+ .AddMetadataReferences(GetAllReferencesNeededForType(typeof(Workflow<,>)));
+
+ // Add the document to the project
+ var document = project.AddDocument("TestDocument.cs", code);
+
+ // Get the syntax tree and create a compilation
+ var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null");
+ var compilation = CSharpCompilation.Create("TestCompilation")
+ .AddSyntaxTrees(syntaxTree)
+ .AddReferences(project.MetadataReferences);
+
+ var compilationWithAnalyzer = compilation.WithAnalyzers(
+ ImmutableArray.Create(
+ new WorkflowRegistrationAnalyzer()));
+
+ // Get diagnostics from the compilation
+ var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync();
+ return (diagnostics, document, workspace);
+ }
+
+ public static MetadataReference[] GetAllReferencesNeededForType(Type type)
+ {
+ var files = GetAllAssemblyFilesNeededForType(type);
+
+ return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray();
+ }
+
+ private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type)
+ {
+ return type.Assembly.GetReferencedAssemblies()
+ .Select(x => Assembly.Load(x.FullName))
+ .Append(type.Assembly)
+ .Select(x => x.Location)
+ .ToImmutableArray();
+ }
+}
diff --git a/test/Dapr.Workflow.Analyzers.Test/Verify.cs b/test/Dapr.Workflow.Analyzers.Test/VerifyAnalyzer.cs
similarity index 58%
rename from test/Dapr.Workflow.Analyzers.Test/Verify.cs
rename to test/Dapr.Workflow.Analyzers.Test/VerifyAnalyzer.cs
index 6a892ef2..2c0204b0 100644
--- a/test/Dapr.Workflow.Analyzers.Test/Verify.cs
+++ b/test/Dapr.Workflow.Analyzers.Test/VerifyAnalyzer.cs
@@ -1,12 +1,10 @@
using Microsoft.CodeAnalysis;
-using System.Collections.Immutable;
-using System.Reflection;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
namespace Dapr.Workflow.Analyzers.Test;
-internal static class Verify
+internal static class VerifyAnalyzer
{
public static DiagnosticResult Diagnostic(string diagnosticId, DiagnosticSeverity diagnosticSeverity)
{
@@ -20,15 +18,25 @@ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticRes
public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected)
{
- var test = new Test { TestCode = source, ReferenceAssemblies = ReferenceAssemblies.Net.Net60 };
+ var test = new Test { TestCode = source };
+
+#if NET6_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60;
+#elif NET7_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net70;
+#elif NET8_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80;
+#elif NET9_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90;
+#endif
if (program != null)
{
test.TestState.Sources.Add(("Program.cs", program));
}
- var metadataReferences = GetAllReferencesNeededForType(typeof(WorkflowActivityAnalyzer)).ToList();
- metadataReferences.AddRange(GetAllReferencesNeededForType(typeof(Workflow<,>)));
+ var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(WorkflowRegistrationAnalyzer)).ToList();
+ metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(Workflow<,>)));
metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
foreach (var reference in metadataReferences)
@@ -40,7 +48,7 @@ public static async Task VerifyAnalyzerAsync(string source, string? program, par
await test.RunAsync(CancellationToken.None);
}
- private class Test : CSharpAnalyzerTest
+ private class Test : CSharpAnalyzerTest
{
public Test()
{
@@ -52,20 +60,4 @@ public Test()
});
}
}
-
- private static MetadataReference[] GetAllReferencesNeededForType(Type type)
- {
- var files = GetAllAssemblyFilesNeededForType(type);
-
- return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray();
- }
-
- private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type)
- {
- return type.Assembly.GetReferencedAssemblies()
- .Select(x => Assembly.Load(x.FullName))
- .Append(type.Assembly)
- .Select(x => x.Location)
- .ToImmutableArray();
- }
}
diff --git a/test/Dapr.Workflow.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Workflow.Analyzers.Test/VerifyCodeFix.cs
new file mode 100644
index 00000000..ba7c89b5
--- /dev/null
+++ b/test/Dapr.Workflow.Analyzers.Test/VerifyCodeFix.cs
@@ -0,0 +1,56 @@
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+
+namespace Dapr.Workflow.Analyzers.Test;
+
+internal class VerifyCodeFix
+{
+ public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new()
+ {
+ var (diagnostics, document, workspace) = await Utilities.GetDiagnosticsAdvanced(code);
+
+ Assert.Single(diagnostics);
+
+ var diagnostic = diagnostics[0];
+
+ var codeFixProvider = new T();
+
+ CodeAction? registeredCodeAction = null;
+
+ var context = new CodeFixContext(document, diagnostic, (codeAction, _) =>
+ {
+ if (registeredCodeAction != null)
+ throw new Exception("Code action was registered more than once");
+
+ registeredCodeAction = codeAction;
+
+ }, CancellationToken.None);
+
+ await codeFixProvider.RegisterCodeFixesAsync(context);
+
+ if (registeredCodeAction == null)
+ throw new Exception("Code action was not registered");
+
+ var operations = await registeredCodeAction.GetOperationsAsync(CancellationToken.None);
+
+ foreach (var operation in operations)
+ {
+ operation.Apply(workspace, CancellationToken.None);
+ }
+
+ var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null");
+ var newCode = (await updatedDocument.GetTextAsync()).ToString();
+
+ // Normalize whitespace
+ string NormalizeWhitespace(string input)
+ {
+ var separator = new[] { ' ', '\r', '\n' };
+ return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries));
+ }
+
+ var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode);
+ var normalizedNewCode = NormalizeWhitespace(newCode);
+
+ Assert.Equal(normalizedExpectedCode, normalizedNewCode);
+ }
+}
diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityAnalyzerTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityAnalyzerTests.cs
deleted file mode 100644
index 07a38e51..00000000
--- a/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityAnalyzerTests.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using Microsoft.CodeAnalysis;
-
-namespace Dapr.Workflow.Analyzers.Test;
-
-public class WorkflowActivityAnalyzerTests
-{
- [Fact]
- public async Task VerifyNotifyActivityNotRegistered()
- {
- var testCode = @"
-using Dapr.Workflow;
-using System.Threading.Tasks;
-
-class OrderProcessingWorkflow : Workflow
-{
- public override async Task RunAsync(WorkflowContext context, OrderPayload order)
- {
- await context.CallActivityAsync(nameof(NotifyActivity), new Notification(""Order received""));
- return new OrderResult(""Order processed"");
- }
-}
-
-class OrderPayload { }
-class OrderResult(string message) { }
-class Notification { public Notification(string message) { } }
-class NotifyActivity { }
-";
-
- var expected = Verify.Diagnostic("DAPR1001", DiagnosticSeverity.Warning)
- .WithSpan(9, 41, 9, 63).WithMessage("The class 'NotifyActivity' is not registered in the DI container");
-
- await Verify.VerifyAnalyzerAsync(testCode, expected);
- }
-
- [Fact]
- public async Task VerifyNotifyActivityRegistered()
- {
- var testCode = @"
- using Dapr.Workflow;
- using Microsoft.Extensions.DependencyInjection;
- using System.Threading.Tasks;
-
- class OrderProcessingWorkflow : Workflow
- {
- public override async Task RunAsync(WorkflowContext context, OrderPayload order)
- {
- await context.CallActivityAsync(nameof(NotifyActivity), new Notification(""Order received""));
- return new OrderResult(""Order processed"");
- }
- }
-
- record OrderPayload { }
- record OrderResult(string message) { }
- record Notification(string Message);
-
- class NotifyActivity : WorkflowActivity
- {
-
- public override Task