diff --git a/all.sln b/all.sln
index b41526cf9..ae51012e0 100644
--- a/all.sln
+++ b/all.sln
@@ -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.Workflow.Analyzers", "src\Dapr.Workflow.Analyzers\Dapr.Workflow.Analyzers.csproj", "{55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Analyzers.Test", "test\Dapr.Workflow.Analyzers.Test\Dapr.Workflow.Analyzers.Test.csproj", "{CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzers", "src\Dapr.Jobs.Analyzer\Dapr.Jobs.Analyzers.csproj", "{28B87C37-4B52-400F-B84D-64F134931BDC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzers.Test", "test\Dapr.Jobs.Analyzer.Test\Dapr.Jobs.Analyzers.Test.csproj", "{CADEAE45-8981-4723-B641-9C28251C7D3B}"
@@ -163,6 +167,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers", "sr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Test", "test\Dapr.Actors.Analyzers.Test\Dapr.Actors.Analyzers.Test.csproj", "{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Analyzers.Common", "test\Dapr.Analyzers.Common\Dapr.Analyzers.Common.csproj", "{7E23E229-6823-4D84-AF3A-AE14CEAEF52A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -411,6 +417,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
+ {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Release|Any CPU.Build.0 = Release|Any CPU
{28B87C37-4B52-400F-B84D-64F134931BDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28B87C37-4B52-400F-B84D-64F134931BDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28B87C37-4B52-400F-B84D-64F134931BDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -427,6 +441,10 @@ Global
{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -500,10 +518,13 @@ 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}
+ {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{28B87C37-4B52-400F-B84D-64F134931BDC} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{CADEAE45-8981-4723-B641-9C28251C7D3B} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{E49C822C-E921-48DF-897B-3E603CA596D2} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{A2C0F203-11FF-4B7F-A94F-B9FD873573FE} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {7E23E229-6823-4D84-AF3A-AE14CEAEF52A} = {DD020B34-460F-455F-8D17-CF4A949F100B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md
index 73d37df67..d04ace66e 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md
@@ -33,6 +33,7 @@ is subject to change in the future as more analyzers are developed.
The following packages will be available via NuGet following the v1.16 Dapr release:
- Dapr.Actors.Analyzers
- Dapr.Jobs.Analyzers
+- Dapr.Workflow.Analyzers
Install each NuGet package on every project where you want the analyzers to run. The package will be installed as a
project dependency and analyzers will run as you write your code or as part of a CI/CD build. The analyzers will flag
@@ -56,13 +57,15 @@ the `EnableNETAnalyzers` property to `false` in your csproj file.
## Available Analyzers
-| Diagnostic ID | Dapr Package | Category | Severity | Version Added | Description | Code Fix Available |
-| -- | -- |------------------| -- | -- | -- | -- |
-| DAPR1401 | Dapr.Actors | Usage | Warning | 1.16 | Actor timer method invocations require the named callback method to exist on type | No |
-| DAPR1402 | Dapr.Actors | Usage | Warning | The actor type is not registered with dependency injection | Yes |
-| DAPR1403 | Dapr.Actors | Interoperability | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors | Yes |
-| DAPR1404 | Dapr.Actors | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors | Yes |
-| DAPR1501 | Dapr.Jobs | Usage | Warning | Job invocations require the MapDaprScheduledJobHandler to be set and configured for each anticipated job on IEndpointRouteBuilder | No |
+| Diagnostic ID | Dapr Package | Category | Severity | Version Added | Description | Code Fix Available |
+| -- | -- |------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------| -- |
+| DAPR1301 | Dapr.Workflow | Usage | Warning | 1.16 | The workflow type is not registered with the dependency injection provider | Yes |
+| DAPR1302 | Dapr.Workflow | Usage | Warning | 1.16 | The workflow activity type is not registered with the dependency injection provider | Yes |
+| DAPR1401 | Dapr.Actors | Usage | Warning | 1.16 | Actor timer method invocations require the named callback method to exist on type | No |
+| DAPR1402 | Dapr.Actors | Usage | Warning | The actor type is not registered with dependency injection | Yes |
+| DAPR1403 | Dapr.Actors | Interoperability | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors | Yes |
+| DAPR1404 | Dapr.Actors | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors | Yes |
+| DAPR1501 | Dapr.Jobs | Usage | Warning | Job invocations require the MapDaprScheduledJobHandler to be set and configured for each anticipated job on IEndpointRouteBuilder | No |
## Analyzer Categories
The following are each of the eligible categories that an analyzer can be assigned to and are modeled after the
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs
index 35b7bb0fa..5795ba3f4 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs
@@ -78,7 +78,7 @@ internal static string DAPR1401Title {
}
///
- /// Looks up a localized string similar to The actor type '{0}' is not registered with dependency injection.
+ /// Looks up a localized string similar to The actor type '{0}' is not registered with the dependency injection provider.
///
internal static string DAPR1402MessageFormat {
get {
@@ -87,7 +87,7 @@ internal static string DAPR1402MessageFormat {
}
///
- /// Looks up a localized string similar to The actor type is not registered with dependency injection.
+ /// Looks up a localized string similar to The actor type is not registered with the dependency injection provider.
///
internal static string DAPR1402Title {
get {
diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx
index b84c1243c..eb9ddcfc4 100644
--- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx
+++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx
@@ -25,10 +25,10 @@
Actor timer method invocations require the named callback '{0}' method to exist on type '{1}'
- The actor type is not registered with dependency injection
+ The actor type is not registered with the dependency injection provider
- The actor type '{0}' is not registered with dependency injection
+ The actor type '{0}' is not registered with the dependency injection provider
Set options.UseJsonSerialization to true to support interoperability with non-.NET actors
diff --git a/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 000000000..ad7437da4
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,8 @@
+## Release 1.16
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+DAPR1301 | Usage | Warning | The workflow class '{0}' is not registered
+DAPR1302 | Usage | Warning | The workflow activity class '{0}' is not registered
\ No newline at end of file
diff --git a/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 000000000..b1b99aaf2
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,3 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/src/Dapr.Workflow.Analyzers/Dapr.Workflow.Analyzers.csproj b/src/Dapr.Workflow.Analyzers/Dapr.Workflow.Analyzers.csproj
new file mode 100644
index 000000000..75aa5b4d2
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/Dapr.Workflow.Analyzers.csproj
@@ -0,0 +1,53 @@
+
+
+
+ netstandard2.0
+
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+ This package contains analyzers for interacting with Dapr workflows.
+ $(NoWarn);RS1038
+
+
+
+
+
+
+
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
diff --git a/src/Dapr.Workflow.Analyzers/Resources.Designer.cs b/src/Dapr.Workflow.Analyzers/Resources.Designer.cs
new file mode 100644
index 000000000..42e524df6
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/Resources.Designer.cs
@@ -0,0 +1,98 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Analyzers {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Dapr.Workflow.Analyzers.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The workflow type '{0}' is not registered with the dependency injection provider.
+ ///
+ internal static string DAPR1301MessageFormat {
+ get {
+ return ResourceManager.GetString("DAPR1301MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The workflow type is not registered with the dependency injection provider.
+ ///
+ internal static string DAPR1301Title {
+ get {
+ return ResourceManager.GetString("DAPR1301Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The workflow activity type '{0}' is not registered with the dependency injection provider.
+ ///
+ internal static string DAPR1302MessageFormat {
+ get {
+ return ResourceManager.GetString("DAPR1302MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The workflow activity type is not registered with the dependency injection provider.
+ ///
+ internal static string DAPR1302Title {
+ get {
+ return ResourceManager.GetString("DAPR1302Title", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Dapr.Workflow.Analyzers/Resources.resx b/src/Dapr.Workflow.Analyzers/Resources.resx
new file mode 100644
index 000000000..12af45514
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/Resources.resx
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ The workflow type is not registered with the dependency injection provider
+
+
+ The workflow type '{0}' is not registered with the dependency injection provider
+
+
+ The workflow activity type is not registered with the dependency injection provider
+
+
+ The workflow activity type '{0}' is not registered with the dependency injection provider
+
+
\ No newline at end of file
diff --git a/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationAnalyzer.cs b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationAnalyzer.cs
new file mode 100644
index 000000000..9141327cb
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationAnalyzer.cs
@@ -0,0 +1,143 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dapr.Workflow.Analyzers;
+
+///
+/// An analyzer for Dapr workflows that validates that each workflow activity is registered with the
+/// dependency injection provider.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class WorkflowActivityRegistrationAnalyzer : DiagnosticAnalyzer
+{
+ internal static readonly DiagnosticDescriptor WorkflowActivityRegistrationDescriptor = new(
+ id: "DAPR1302",
+ title: new LocalizableResourceString(nameof(Resources.DAPR1302Title), Resources.ResourceManager,
+ typeof(Resources)),
+ messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1302MessageFormat), Resources.ResourceManager,
+ typeof(Resources)),
+ category: "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ ///
+ /// Returns a set of descriptors for the diagnostics that this analyzer is capable of producing.
+ ///
+ public override ImmutableArray SupportedDiagnostics =>
+ [
+ WorkflowActivityRegistrationDescriptor
+ ];
+
+
+ ///
+ /// Called once at session start to register actions in the analysis context.
+ ///
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeWorkflowActivityRegistration, SyntaxKind.InvocationExpression);
+ }
+
+ private static 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 not InvocationExpressionSyntax nameofInvocation)
+ {
+ return;
+ }
+
+ var activityName = nameofInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString().Trim('"');
+ if (activityName == null)
+ {
+ return;
+ }
+
+ bool isRegistered = CheckIfActivityIsRegistered(activityName, context.SemanticModel);
+ if (isRegistered)
+ {
+ return;
+ }
+
+ var diagnostic = Diagnostic.Create(WorkflowActivityRegistrationDescriptor, 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")
+ {
+ continue;
+ }
+
+ if (memberAccess.Name is not GenericNameSyntax typeArgumentList ||
+ typeArgumentList.TypeArgumentList.Arguments.Count <= 0)
+ {
+ continue;
+ }
+
+ if (typeArgumentList.TypeArgumentList.Arguments[0] is not IdentifierNameSyntax typeArgument)
+ {
+ continue;
+ }
+
+ if (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 000000000..77ed6c9cf
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationCodeFixProvider.cs
@@ -0,0 +1,128 @@
+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))]
+public sealed class WorkflowActivityRegistrationCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ["DAPR1002"];
+
+ ///
+ /// Registers the code fix for the diagnostic.
+ ///
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ const string title = "Register Dapr workflow activity";
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title,
+ createChangedDocument: c => RegisterWorkflowActivityAsync(context.Document, context.Diagnostics.First(), c),
+ equivalenceKey: title),
+ context.Diagnostics);
+ return Task.CompletedTask;
+ }
+
+ private static 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 == 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 is not { Body: 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 000000000..91593c8ff
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs
@@ -0,0 +1,122 @@
+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
+{
+ internal static readonly DiagnosticDescriptor WorkflowDiagnosticDescriptor = new(
+ id: "DAPR1301",
+ title: new LocalizableResourceString(nameof(Resources.DAPR1301Title), Resources.ResourceManager, typeof(Resources)),
+ messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1301MessageFormat), Resources.ResourceManager, typeof(Resources)),
+ category: "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// Gets the supported diagnostics for this analyzer.
+ ///
+ public override ImmutableArray SupportedDiagnostics => [WorkflowDiagnosticDescriptor];
+
+ ///
+ /// Initializes the analyzer.
+ ///
+ /// The analysis context.
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeWorkflowRegistration, SyntaxKind.InvocationExpression);
+ }
+
+ private static 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 not InvocationExpressionSyntax nameofInvocation)
+ {
+ return;
+ }
+
+ var workflowName = nameofInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString().Trim('"');
+ if (workflowName == null)
+ {
+ return;
+ }
+
+ bool isRegistered = CheckIfWorkflowIsRegistered(workflowName, context.SemanticModel);
+ if (isRegistered)
+ {
+ return;
+ }
+
+ var diagnostic = Diagnostic.Create(WorkflowDiagnosticDescriptor, firstArgument.GetLocation(), workflowName);
+ 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")
+ {
+ continue;
+ }
+
+ if (memberAccess.Name is not GenericNameSyntax typeArgumentList ||
+ typeArgumentList.TypeArgumentList.Arguments.Count <= 0)
+ {
+ continue;
+ }
+
+ if (typeArgumentList.TypeArgumentList.Arguments[0] is not IdentifierNameSyntax typeArgument)
+ {
+ continue;
+ }
+
+ if (typeArgument.Identifier.Text == workflowName)
+ {
+ 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 000000000..34aebca3d
--- /dev/null
+++ b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationCodeFixProvider.cs
@@ -0,0 +1,242 @@
+using System.Collections.Immutable;
+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))]
+public sealed class WorkflowRegistrationCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ["DAPR1001"];
+
+ ///
+ /// Registers the code fix for the diagnostic.
+ ///
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ const string title = "Register Dapr 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 || root is null)
+ {
+ return document;
+ }
+
+ // Get the semantic model
+ var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
+
+ // Extract the workflow type name
+ var workflowTypeSyntax = oldInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression;
+
+ if (workflowTypeSyntax == null)
+ {
+ return document;
+ }
+
+ // Get the symbol for the workflow type
+ if (semanticModel.GetSymbolInfo(workflowTypeSyntax, cancellationToken).Symbol is not INamedTypeSymbol
+ workflowTypeSymbol)
+ {
+ return document;
+ }
+
+ // Get the fully qualified name
+ var workflowType = workflowTypeSymbol.ToDisplayString(new SymbolDisplayFormat(
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces));
+
+ if (string.IsNullOrEmpty(workflowType))
+ {
+ return document;
+ }
+
+ // Get the compilation
+ var compilation = await document.Project.GetCompilationAsync(cancellationToken);
+
+ if (compilation == null)
+ {
+ return document;
+ }
+
+ var (targetDocument, addDaprWorkflowInvocation) = await FindAddDaprWorkflowInvocationAsync(document.Project, cancellationToken);
+
+ if (addDaprWorkflowInvocation == null)
+ {
+ (targetDocument, addDaprWorkflowInvocation) = await CreateAddDaprWorkflowInvocation(document.Project, cancellationToken);
+ }
+
+ if (addDaprWorkflowInvocation == null)
+ {
+ return document;
+ }
+
+ var targetRoot = await addDaprWorkflowInvocation.SyntaxTree.GetRootAsync(cancellationToken);
+
+ if (targetRoot == null || targetDocument == null)
+ return document;
+
+ // Find the options lambda block
+ var optionsLambda = addDaprWorkflowInvocation.ArgumentList.Arguments
+ .Select(arg => arg.Expression)
+ .OfType()
+ .FirstOrDefault();
+
+ if (optionsLambda is not { Body: 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;
+ }
+
+ private static async Task<(Document?, InvocationExpressionSyntax?)> FindAddDaprWorkflowInvocationAsync(Project project, CancellationToken cancellationToken)
+ {
+ var compilation = await project.GetCompilationAsync(cancellationToken);
+
+ foreach (var syntaxTree in compilation!.SyntaxTrees)
+ {
+ var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken);
+
+ var addDaprWorkflowInvocation = syntaxRoot.DescendantNodes()
+ .OfType()
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "AddDaprWorkflow");
+
+ if (addDaprWorkflowInvocation == null)
+ {
+ continue;
+ }
+
+ var document = project.GetDocument(addDaprWorkflowInvocation.SyntaxTree);
+ return (document, addDaprWorkflowInvocation);
+ }
+
+ return (null, null);
+ }
+
+ private async Task<(Document?, InvocationExpressionSyntax?)> CreateAddDaprWorkflowInvocation(Project project, CancellationToken cancellationToken)
+ {
+ var createBuilderInvocation = await FindCreateBuilderInvocationAsync(project, cancellationToken);
+
+ var variableDeclarator = createBuilderInvocation?.Ancestors()
+ .OfType()
+ .FirstOrDefault();
+
+ var builderVariable = variableDeclarator?.Identifier.Text;
+
+ if (createBuilderInvocation == null)
+ {
+ return (null, null);
+ }
+
+ var targetRoot = await createBuilderInvocation.SyntaxTree.GetRootAsync(cancellationToken);
+ var document = project.GetDocument(createBuilderInvocation.SyntaxTree);
+
+ if (createBuilderInvocation.Expression is not MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax })
+ {
+ return (null, null);
+ }
+
+ var addDaprWorkflowStatement = SyntaxFactory.ParseStatement($"{builderVariable}.Services.AddDaprWorkflow(options => {{ }});");
+
+ if (createBuilderInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock)
+ {
+ var firstChild = parentBlock.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax);
+ var newParentBlock = parentBlock.InsertNodesAfter(firstChild, new[] { addDaprWorkflowStatement });
+ targetRoot = targetRoot.ReplaceNode(parentBlock, newParentBlock);
+ }
+ else
+ {
+ var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault();
+ if (compilationUnitSyntax != null)
+ {
+ var firstChild = compilationUnitSyntax.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax);
+ var globalStatement = SyntaxFactory.GlobalStatement(addDaprWorkflowStatement);
+ var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(firstChild, [globalStatement]);
+ targetRoot = targetRoot.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax);
+ }
+ }
+
+ var addDaprWorkflowInvocation = targetRoot?.DescendantNodes()
+ .OfType()
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "AddDaprWorkflow");
+
+ return (document, addDaprWorkflowInvocation);
+
+ }
+
+ private static async Task FindCreateBuilderInvocationAsync(Project project, CancellationToken cancellationToken)
+ {
+ var compilation = await project.GetCompilationAsync(cancellationToken);
+
+ foreach (var syntaxTree in compilation!.SyntaxTrees)
+ {
+ var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken);
+
+ // Find the invocation expression for WebApplication.CreateBuilder()
+ var createBuilderInvocation = syntaxRoot.DescendantNodes()
+ .OfType()
+ .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax
+ {
+ Expression: IdentifierNameSyntax
+ {
+ Identifier.Text: "WebApplication"
+ },
+ Name.Identifier.Text: "CreateBuilder"
+ });
+
+ if (createBuilderInvocation != null)
+ {
+ return createBuilderInvocation;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj
index 088595463..0598e91fe 100644
--- a/src/Dapr.Workflow/Dapr.Workflow.csproj
+++ b/src/Dapr.Workflow/Dapr.Workflow.csproj
@@ -17,7 +17,6 @@
-
\ No newline at end of file
diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs
index 63a775b15..e0cf4ae34 100644
--- a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs
+++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs
@@ -11,6 +11,9 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Actors.Analyzers.Test;
+using Dapr.Analyzers.Common;
+
namespace Dapr.Actors.Analyzers.Tests;
public class ActorRegistrationAnalyzerTests
@@ -29,9 +32,10 @@ public TestActor(ActorHost host) : base(host)
""";
var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration)
- .WithSpan(2, 7, 2, 16).WithMessage("The actor type 'TestActor' is not registered with dependency injection");
+ .WithSpan(2, 7, 2, 16).WithMessage("The actor type 'TestActor' is not registered with the dependency injection provider");
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode, expected);
}
[Fact]
@@ -47,9 +51,10 @@ public TestActor(Dapr.Actors.Runtime.ActorHost host) : base(host)
""";
var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration)
- .WithSpan(1, 7, 1, 16).WithMessage("The actor type 'TestActor' is not registered with dependency injection");
+ .WithSpan(1, 7, 1, 16).WithMessage("The actor type 'TestActor' is not registered with the dependency injection provider");
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode, expected);
}
@@ -67,9 +72,10 @@ public TestActor(alias.ActorHost host) : base(host)
""";
var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration)
- .WithSpan(2, 15, 2, 24).WithMessage("The actor type 'TestActor' is not registered with dependency injection");
+ .WithSpan(2, 15, 2, 24).WithMessage("The actor type 'TestActor' is not registered with the dependency injection provider");
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode, expected);
}
[Fact]
@@ -102,7 +108,8 @@ public TestActor(ActorHost host) : base(host)
}
""";
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
[Fact]
@@ -138,7 +145,8 @@ public TestActor(ActorHost host) : base(host)
}
""";
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
[Fact]
@@ -175,6 +183,7 @@ public TestActor(ActorHost host) : base(host)
}
""";
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
}
diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs
index 9ae4afc68..bf2ff0a80 100644
--- a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs
+++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs
@@ -11,6 +11,9 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Actors.Analyzers.Test;
+using Dapr.Analyzers.Common;
+
namespace Dapr.Actors.Analyzers.Tests;
public class ActorRegistrationCodeFixProviderTests
@@ -77,7 +80,8 @@ public TestActor(ActorHost host) : base(host)
""";
- await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ await VerifyCodeFix.RunTest(code, expectedChangedCode,
+ typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers());
}
[Fact]
@@ -132,7 +136,8 @@ public TestActor(ActorHost host) : base(host)
""";
- await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ await VerifyCodeFix.RunTest(code, expectedChangedCode,
+ typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers());
}
[Fact]
@@ -181,6 +186,7 @@ public TestActor(ActorHost host) : base(host)
""";
- await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ await VerifyCodeFix.RunTest(code, expectedChangedCode,
+ typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers());
}
}
diff --git a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj
index 8d9458e26..a73518742 100644
--- a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj
+++ b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj
@@ -5,17 +5,16 @@
enable
false
true
- Dapr.Actors.Analyzers.Tests
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -34,6 +33,7 @@
+
diff --git a/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs
index ed649f986..592f32085 100644
--- a/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs
+++ b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs
@@ -11,6 +11,9 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Actors.Analyzers.Test;
+using Dapr.Analyzers.Common;
+
namespace Dapr.Actors.Analyzers.Tests;
public class MappedActorHandlersAnalyzerTests
@@ -40,7 +43,8 @@ public static void Main()
var expected = VerifyAnalyzer.Diagnostic(MappedActorHandlersAnalyzer.DiagnosticDescriptorMapActorsHandlers)
.WithSpan(10, 25, 13, 27).WithMessage("Call app.MapActorsHandlers to map endpoints for Dapr actors");
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode, expected);
}
[Fact]
@@ -66,6 +70,7 @@ public static void Main()
}
""";
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
}
diff --git a/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs
index 7c66a2656..b7bf0209f 100644
--- a/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs
+++ b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs
@@ -11,6 +11,9 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Actors.Analyzers.Test;
+using Dapr.Analyzers.Common;
+
namespace Dapr.Actors.Analyzers.Tests;
public class MappedActorHandlersCodeFixProviderTests
@@ -46,34 +49,35 @@ public TestActor(ActorHost host) : base(host)
""";
const string expectedChangedCode = """
- using Dapr.Actors.Runtime;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.Extensions.DependencyInjection;
-
- public static class Program
- {
- public static void Main()
- {
- var builder = WebApplication.CreateBuilder();
-
- builder.Services.AddActors(options =>
- {
- options.Actors.RegisterActor();
- options.UseJsonSerialization = true;
- });
- var app = builder.Build();
- app.MapActorsHandlers();
- }
- }
- class TestActor : Actor
- {
- public TestActor(ActorHost host) : base(host)
- {
- }
- }
- """;
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+ var app = builder.Build();
+ app.MapActorsHandlers();
+ }
+ }
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+ """;
- await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ await VerifyCodeFix.RunTest(code, expectedChangedCode,
+ typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers());
}
[Fact]
@@ -103,29 +107,30 @@ public TestActor(ActorHost host) : base(host)
""";
const string expectedChangedCode = """
-
- using Dapr.Actors.Runtime;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.Extensions.DependencyInjection;
-
- var builder = WebApplication.CreateBuilder();
-
- builder.Services.AddActors(options =>
- {
- options.Actors.RegisterActor();
- options.UseJsonSerialization = true;
- });
- var app = builder.Build();
- app.MapActorsHandlers();
- class TestActor : Actor
- {
- public TestActor(ActorHost host) : base(host)
- {
- }
- }
-
- """;
+
+ using Dapr.Actors.Runtime;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.Extensions.DependencyInjection;
+
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddActors(options =>
+ {
+ options.Actors.RegisterActor();
+ options.UseJsonSerialization = true;
+ });
+ var app = builder.Build();
+ app.MapActorsHandlers();
+ class TestActor : Actor
+ {
+ public TestActor(ActorHost host) : base(host)
+ {
+ }
+ }
+
+ """;
- await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ await VerifyCodeFix.RunTest(code, expectedChangedCode,
+ typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers());
}
}
diff --git a/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs
index 2fd8f5e77..f186daac2 100644
--- a/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs
+++ b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs
@@ -11,6 +11,9 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Actors.Analyzers.Test;
+using Dapr.Analyzers.Common;
+
namespace Dapr.Actors.Analyzers.Tests;
public class PreferActorJsonSerializationAnalyzerTests
@@ -42,7 +45,8 @@ public static void Main()
.WithSpan(10, 25, 12, 27)
.WithMessage("Set options.UseJsonSerialization to true to support interoperability with non-.NET actors");
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode, expected);
}
[Fact]
@@ -68,6 +72,7 @@ public static void Main()
}
""";
- await VerifyAnalyzer.VerifyAnalyzerAsync(testCode);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
}
diff --git a/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs
index 0a7288c24..f8d89ef39 100644
--- a/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs
+++ b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs
@@ -11,6 +11,9 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Actors.Analyzers.Test;
+using Dapr.Analyzers.Common;
+
namespace Dapr.Actors.Analyzers.Tests;
public class PreferActorJsonSerializationCodeFixProviderTests
@@ -57,6 +60,7 @@ public static void Main()
}
""";
- await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ await VerifyCodeFix.RunTest(code, expectedChangedCode,
+ typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers());
}
}
diff --git a/test/Dapr.Actors.Analyzers.Test/Utilities.cs b/test/Dapr.Actors.Analyzers.Test/Utilities.cs
new file mode 100644
index 000000000..cad509db3
--- /dev/null
+++ b/test/Dapr.Actors.Analyzers.Test/Utilities.cs
@@ -0,0 +1,48 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Collections.Immutable;
+using Dapr.Analyzers.Common;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Dapr.Actors.Analyzers.Test;
+
+internal static class Utilities
+{
+ internal static ImmutableArray GetAnalyzers() =>
+ [
+ new ActorRegistrationAnalyzer(),
+ new MappedActorHandlersAnalyzer(),
+ new PreferActorJsonSerializationAnalyzer(),
+ new TimerCallbackMethodPresentAnalyzer()
+ ];
+
+ internal static IReadOnlyList GetReferences()
+ {
+ var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList();
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer)));
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer)));
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer)));
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions)));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(ActorsEndpointRouteBuilderExtensions).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(ActorsServiceCollectionExtensions).Assembly.Location));
+ return metadataReferences;
+ }
+}
diff --git a/test/Dapr.Analyzers.Common/Dapr.Analyzers.Common.csproj b/test/Dapr.Analyzers.Common/Dapr.Analyzers.Common.csproj
new file mode 100644
index 000000000..8a225e612
--- /dev/null
+++ b/test/Dapr.Analyzers.Common/Dapr.Analyzers.Common.csproj
@@ -0,0 +1,24 @@
+
+
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.Actors.Analyzers.Test/TestUtilities.cs b/test/Dapr.Analyzers.Common/TestUtilities.cs
similarity index 65%
rename from test/Dapr.Actors.Analyzers.Test/TestUtilities.cs
rename to test/Dapr.Analyzers.Common/TestUtilities.cs
index acfec6baa..b6db51472 100644
--- a/test/Dapr.Actors.Analyzers.Test/TestUtilities.cs
+++ b/test/Dapr.Analyzers.Common/TestUtilities.cs
@@ -13,19 +13,21 @@
using System.Collections.Immutable;
using System.Reflection;
-using Microsoft.AspNetCore.Builder;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-namespace Dapr.Actors.Analyzers.Tests;
+namespace Dapr.Analyzers.Common;
internal static class TestUtilities
{
- public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code)
+ internal static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)>
+ GetDiagnosticsAdvanced(
+ string code,
+ string assemblyLocation,
+ IReadOnlyList additionalMetadataReferences,
+ ImmutableArray analyzers)
{
var workspace = new AdhocWorkspace();
@@ -42,47 +44,40 @@ internal static class TestUtilities
{
{ "CS1701", ReportDiagnostic.Suppress }
}))
- .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default))
- .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
- .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication)))
- .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost)))
- .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsEndpointRouteBuilderExtensions)))
- .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions)));
+ .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, CancellationToken.None))
+ .AddMetadataReference(MetadataReference.CreateFromFile(assemblyLocation))
+ .AddMetadataReferences(additionalMetadataReferences);
// 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 syntaxTree = await document.GetSyntaxTreeAsync() ??
+ throw new InvalidOperationException("Syntax tree is null");
var compilation = CSharpCompilation.Create("TestCompilation")
.AddSyntaxTrees(syntaxTree)
.AddReferences(project.MetadataReferences)
.WithOptions(project.CompilationOptions!);
- var compilationWithAnalyzer = compilation.WithAnalyzers(
- [
- new ActorRegistrationAnalyzer(),
- new MappedActorHandlersAnalyzer(),
- new PreferActorJsonSerializationAnalyzer(),
- new TimerCallbackMethodPresentAnalyzer()
- ]);
+ var compilationWithAnalyzer = compilation.WithAnalyzers(analyzers);
// Get diagnostics from the compilation
var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync();
return (diagnostics, document, workspace);
}
- public static MetadataReference[] GetAllReferencesNeededForType(Type type)
+ internal static MetadataReference[] GetAllReferencesNeededForType(Type type)
{
var files = GetAllAssemblyFilesNeededForType(type);
return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray();
}
- private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) => type.Assembly
- .GetReferencedAssemblies()
- .Select(x => Assembly.Load(x.FullName))
- .Append(type.Assembly)
- .Select(x => x.Location)
- .ToImmutableArray();
+ private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) => [
+ ..type.Assembly
+ .GetReferencedAssemblies()
+ .Select(x => Assembly.Load(x.FullName))
+ .Append(type.Assembly)
+ .Select(x => x.Location)
+ ];
}
diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Analyzers.Common/VerifyAnalyzer.cs
similarity index 62%
rename from test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs
rename to test/Dapr.Analyzers.Common/VerifyAnalyzer.cs
index 87136e411..f2d725061 100644
--- a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs
+++ b/test/Dapr.Analyzers.Common/VerifyAnalyzer.cs
@@ -11,27 +11,24 @@
// limitations under the License.
// ------------------------------------------------------------------------
-using Microsoft.AspNetCore.Builder;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-namespace Dapr.Actors.Analyzers.Tests;
+namespace Dapr.Analyzers.Common;
-internal static class VerifyAnalyzer
+internal class VerifyAnalyzer(IReadOnlyList metadataReferences)
{
public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => new(descriptor);
- public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ public async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
where TAnalyzer : DiagnosticAnalyzer, new()
{
await VerifyAnalyzerAsync(source, null, expected);
}
- public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected)
+ public async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected)
where TAnalyzer : DiagnosticAnalyzer, new()
{
var test = new Test { TestCode = source };
@@ -47,14 +44,14 @@ public static async Task VerifyAnalyzerAsync(string source, string? p
test.TestState.Sources.Add(("Program.cs", program));
}
- var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList();
- metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer)));
- metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer)));
- metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer)));
- metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions)));
- metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
- metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location));
- metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location));
+ // var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList();
+ // metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer)));
+ // metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer)));
+ // metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer)));
+ // metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions)));
+ // metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
+ // metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location));
+ // metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location));
foreach (var reference in metadataReferences)
{
@@ -65,8 +62,6 @@ public static async Task VerifyAnalyzerAsync(string source, string? p
await test.RunAsync(CancellationToken.None);
}
-
-
private sealed class Test : CSharpAnalyzerTest
where TAnalyzer : DiagnosticAnalyzer, new()
{
@@ -81,3 +76,4 @@ public Test()
}
}
}
+
diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Analyzers.Common/VerifyCodeFix.cs
similarity index 75%
rename from test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs
rename to test/Dapr.Analyzers.Common/VerifyCodeFix.cs
index d09e427af..aac71aa9e 100644
--- a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs
+++ b/test/Dapr.Analyzers.Common/VerifyCodeFix.cs
@@ -11,16 +11,26 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Xunit;
-namespace Dapr.Actors.Analyzers.Tests;
+namespace Dapr.Analyzers.Common;
internal static class VerifyCodeFix
{
- public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new()
+ public static async Task RunTest(
+ string code,
+ string expectedChangedCode,
+ string assemblyLocation,
+ IReadOnlyList metadataReferences,
+ ImmutableArray analyzers) where T : CodeFixProvider, new()
{
- var (diagnostics, document, workspace) = await TestUtilities.GetDiagnosticsAdvanced(code);
+ var (diagnostics, document, workspace) =
+ await TestUtilities.GetDiagnosticsAdvanced(code, assemblyLocation, metadataReferences, analyzers);
Assert.Single(diagnostics);
@@ -51,7 +61,8 @@ internal static class VerifyCodeFix
operation.Apply(workspace, CancellationToken.None);
}
- var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null");
+ var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ??
+ throw new Exception("Updated document is null");
var newCode = (await updatedDocument.GetTextAsync()).ToString();
var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode);
@@ -63,7 +74,7 @@ internal static class VerifyCodeFix
// Normalize whitespace
string NormalizeWhitespace(string input)
{
- var separator = new[] { ' ', '\r', '\n' };
+ char[] separator = [' ', '\r', '\n'];
return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries));
}
}
diff --git a/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj b/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj
index 57ca6b87a..339b39d63 100644
--- a/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj
+++ b/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj
@@ -26,6 +26,7 @@
+
diff --git a/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs b/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs
index 19a34cbc4..e8d4cb3e2 100644
--- a/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs
+++ b/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs
@@ -1,9 +1,5 @@
-using Dapr.Jobs.Models;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp.Testing;
+using Dapr.Analyzers.Common;
using Microsoft.CodeAnalysis.Testing;
-using Microsoft.Extensions.Hosting;
namespace Dapr.Jobs.Analyzers.Test;
@@ -13,7 +9,7 @@ public class DaprJobsAnalyzerAnalyzerTests
#if NET8_0
private static readonly ReferenceAssemblies referenceAssemblies = ReferenceAssemblies.Net.Net80;
#elif NET9_0
- private static readonly ReferenceAssemblies referenceAssemblies = ReferenceAssemblies.Net.Net90;
+ private static readonly ReferenceAssemblies referenceAssemblies = ReferenceAssemblies.Net.Net90;
#endif
[Fact]
@@ -47,11 +43,13 @@ public static void Main()
}
""";
- await VerifyAnalyzerAsync(testCode,
- new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule)
- .WithSpan(22, 25, 23, 83)
- .WithMessage(
- "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder"));
+ var expected = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule)
+ .WithSpan(22, 25, 23, 83)
+ .WithMessage(
+ "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder");
+
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode, expected);
}
[Fact]
@@ -82,7 +80,8 @@ public static void Main()
}
""";
- await VerifyAnalyzerAsync(testCode);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
[Fact]
@@ -118,13 +117,15 @@ public static void Main()
}
""";
- await VerifyAnalyzerAsync(testCode,
- new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule)
- .WithSpan(22, 25, 23, 83)
- .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder"),
- new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule)
- .WithSpan(24, 25, 25, 83)
- .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob2' on IEndpointRouteBuilder"));
+ var expected1 = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule)
+ .WithSpan(22, 25, 23, 83)
+ .WithMessage(
+ "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder");
+ var expected2 = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule)
+ .WithSpan(24, 25, 25, 83)
+ .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob2' on IEndpointRouteBuilder");
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode, expected1, expected2);
}
[Fact]
@@ -165,7 +166,8 @@ public static void Main()
}
""";
- await VerifyAnalyzerAsync(testCode);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
[Fact]
@@ -206,7 +208,8 @@ await daprJobsClient.ScheduleJobAsync("myJob2", DaprJobSchedule.FromDuration(Tim
}
""";
- await VerifyAnalyzerAsync(testCode);
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
[Fact]
@@ -245,28 +248,7 @@ public static Task ScheduleJobAsync(string jobNAme)
""";
- await VerifyAnalyzerAsync(testCode);
- }
-
- private static async Task VerifyAnalyzerAsync(string testCode, params DiagnosticResult[] expectedDiagnostics)
- {
- var test = new CSharpAnalyzerTest
- {
- TestCode = testCode
- };
-
- test.TestState.ReferenceAssemblies = referenceAssemblies;
-
- test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location));
- test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobsClient).Assembly.Location));
- test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobSchedule).Assembly.Location));
- test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location));
- test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location));
- test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(
- typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location));
- test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location));
-
- test.ExpectedDiagnostics.AddRange(expectedDiagnostics);
- await test.RunAsync();
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode);
}
}
diff --git a/test/Dapr.Jobs.Analyzer.Test/Utilities.cs b/test/Dapr.Jobs.Analyzer.Test/Utilities.cs
new file mode 100644
index 000000000..f3fcf5079
--- /dev/null
+++ b/test/Dapr.Jobs.Analyzer.Test/Utilities.cs
@@ -0,0 +1,43 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Collections.Immutable;
+using Dapr.Analyzers.Common;
+using Dapr.Jobs.Models;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.Extensions.Hosting;
+
+namespace Dapr.Jobs.Analyzers.Test;
+
+internal static class Utilities
+{
+ internal static ImmutableArray GetAnalyzers() =>
+ [
+ new MapDaprScheduledJobHandlerAnalyzer()
+ ];
+
+ internal static IReadOnlyList GetReferences()
+ {
+ var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(MapDaprScheduledJobHandlerAnalyzer)).ToList();
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobsClient).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobSchedule).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location));
+ return metadataReferences;
+ }
+}
diff --git a/test/Dapr.Workflow.Analyzers.Test/Dapr.Workflow.Analyzers.Test.csproj b/test/Dapr.Workflow.Analyzers.Test/Dapr.Workflow.Analyzers.Test.csproj
new file mode 100644
index 000000000..83139ac4e
--- /dev/null
+++ b/test/Dapr.Workflow.Analyzers.Test/Dapr.Workflow.Analyzers.Test.csproj
@@ -0,0 +1,39 @@
+
+
+
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.Workflow.Analyzers.Test/GlobalUsings.cs b/test/Dapr.Workflow.Analyzers.Test/GlobalUsings.cs
new file mode 100644
index 000000000..48f0c59b2
--- /dev/null
+++ b/test/Dapr.Workflow.Analyzers.Test/GlobalUsings.cs
@@ -0,0 +1,14 @@
+// ------------------------------------------------------------------------
+// Copyright 2023 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+global using Xunit;
\ No newline at end of file
diff --git a/test/Dapr.Workflow.Analyzers.Test/TestModels.cs b/test/Dapr.Workflow.Analyzers.Test/TestModels.cs
new file mode 100644
index 000000000..696a289b8
--- /dev/null
+++ b/test/Dapr.Workflow.Analyzers.Test/TestModels.cs
@@ -0,0 +1,4 @@
+namespace Dapr.Workflow.Analyzers.Test;
+
+internal record OrderPayload(string OrderId, string CustomerId);
+internal record class OrderResult(string Result);
diff --git a/test/Dapr.Workflow.Analyzers.Test/Utilities.cs b/test/Dapr.Workflow.Analyzers.Test/Utilities.cs
new file mode 100644
index 000000000..64be64dbd
--- /dev/null
+++ b/test/Dapr.Workflow.Analyzers.Test/Utilities.cs
@@ -0,0 +1,35 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.Reflection;
+using Dapr.Analyzers.Common;
+using Microsoft.Extensions.Hosting;
+
+namespace Dapr.Workflow.Analyzers.Test;
+
+internal static class Utilities
+{
+ internal static ImmutableArray GetAnalyzers() =>
+ [
+ new WorkflowRegistrationAnalyzer(),
+ new WorkflowActivityRegistrationAnalyzer()
+ ];
+
+ internal static IReadOnlyList GetReferences()
+ {
+ var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowActivityRegistrationAnalyzer)).ToList();
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowRegistrationAnalyzer)));
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimeSpan)));
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(Workflow<,>)));
+ metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowActivity<,>)));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Task).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprWorkflowClient).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location));
+ return metadataReferences;
+ }
+}
diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationAnalyzerTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationAnalyzerTests.cs
new file mode 100644
index 000000000..9e96ccb5a
--- /dev/null
+++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationAnalyzerTests.cs
@@ -0,0 +1,99 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Dapr.Analyzers.Common;
+
+namespace Dapr.Workflow.Analyzers.Test;
+
+public sealed class WorkflowActivityRegistrationAnalyzerTests
+{
+ [Fact]
+ public async Task VerifyActivityNotRegistered()
+ {
+ const string 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");
+ }
+ }
+
+ record OrderPayload { }
+ record OrderResult(string message) { }
+ record Notification { public Notification(string message) { } }
+ class NotifyActivity { }
+ """;
+
+ var expected = VerifyAnalyzer.Diagnostic(WorkflowActivityRegistrationAnalyzer.WorkflowActivityRegistrationDescriptor)
+ .WithSpan(8, 57, 8, 79).WithMessage("The workflow activity type 'NotifyActivity' is not registered with the dependency injection provider");
+
+ var analyzer = new VerifyAnalyzer(Utilities.GetReferences());
+ await analyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task VerifyActivityRegistered()
+ {
+ const string 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