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 RunAsync(WorkflowActivityContext context, Notification notification) + { + return Task.FromResult(null); + } + } + """; + + const string startupCode = """ + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddDaprWorkflow(options => + { + options.RegisterActivity(); + }); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, startupCode); + } +} diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs new file mode 100644 index 000000000..9944a578e --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs @@ -0,0 +1,93 @@ +using Dapr.Analyzers.Common; + +namespace Dapr.Workflow.Analyzers.Test; + +public sealed class WorkflowActivityRegistrationCodeFixProviderTests +{ + [Fact] + public async Task VerifyWorkflowActivityRegistrationCodeFix() + { + const string code = """ + using Dapr.Workflow; + using System; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => + { + }); + } + } + + 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) { } }; + internal sealed class NotifyActivity : WorkflowActivity + { + public override async Task RunAsync(WorkflowActivityContext context, string input) + { + await Task.Delay(TimeSpan.FromSeconds(15)); + return true; + } + } + """; + + const string expectedChangedCode = """ + using Dapr.Workflow; + using System; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => + { + options.RegisterActivity(); + }); + } + } + + 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) { } }; + internal sealed class NotifyActivity : WorkflowActivity + { + public override async Task RunAsync(WorkflowActivityContext context, string input) + { + await Task.Delay(TimeSpan.FromSeconds(15)); + return true; + } + } + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode, typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); + } +} diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationAnalyzerTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationAnalyzerTests.cs new file mode 100644 index 000000000..ee684db2e --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationAnalyzerTests.cs @@ -0,0 +1,79 @@ +using Dapr.Analyzers.Common; + +namespace Dapr.Workflow.Analyzers.Test; + +public sealed class WorkflowRegistrationAnalyzerTests +{ + [Fact] + public async Task VerifyWorkflowNotRegistered() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + class OrderProcessingWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + return new OrderResult("Order processed"); + } + } + + class UseWorkflow() + { + public async Task RunWorkflow(DaprWorkflowClient client, OrderPayload order) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, order); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + var expected = VerifyAnalyzer.Diagnostic( WorkflowRegistrationAnalyzer.WorkflowDiagnosticDescriptor) + .WithSpan(16, 63, 16, 94).WithMessage("The workflow type 'OrderProcessingWorkflow' is not registered with the dependency injection provider"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyWorkflowRegistered() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + class OrderProcessingWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + return new OrderResult("Order processed"); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + const string startupCode = """ + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + }); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, startupCode); + } +} diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationCodeFixProviderTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationCodeFixProviderTests.cs new file mode 100644 index 000000000..91339aea6 --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationCodeFixProviderTests.cs @@ -0,0 +1,226 @@ +using Dapr.Analyzers.Common; + +namespace Dapr.Workflow.Analyzers.Test; + +public sealed class WorkflowRegistrationCodeFixProviderTests +{ + [Fact] + public async Task RegisterWorkflow() + { + const string code = """ + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => + { + }); + } + + private static async Task ScheduleWorkflow(DaprWorkflowClient client) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, new OrderPayload()); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + + """; + + const string expectedChangedCode = """ + + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + }); + } + + private static async Task ScheduleWorkflow(DaprWorkflowClient client) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, new OrderPayload()); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode, typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); + } + + [Fact] + public async Task RegisterWorkflow_WhenAddDaprWorkflowIsNotFound() + { + const string code = """ + using Dapr.Workflow; + using Microsoft.AspNetCore.Builder; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + } + + private static async Task ScheduleWorkflow(DaprWorkflowClient client) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, new OrderPayload()); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + const string expectedChangedCode = """ + using Dapr.Workflow; + using Microsoft.AspNetCore.Builder; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + }); + + var app = builder.Build(); + } + + private static async Task ScheduleWorkflow(DaprWorkflowClient client) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, new OrderPayload()); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode, typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); + } + + [Fact] + public async Task RegisterWorkflow_WhenAddDaprWorkflowIsNotFound_TopLevelStatements() + { + const string code = """ + using Dapr.Workflow; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + + var workflowClient = app.Services.GetRequiredService(); + await workflowClient.ScheduleNewWorkflowAsync(nameof(TestNamespace.OrderProcessingWorkflow), null, new OrderPayload()); + + namespace TestNamespace + { + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + const string expectedChangedCode = """ + using Dapr.Workflow; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + }); + + var app = builder.Build(); + + var workflowClient = app.Services.GetRequiredService(); + await workflowClient.ScheduleNewWorkflowAsync(nameof(TestNamespace.OrderProcessingWorkflow), null, new OrderPayload()); + + namespace TestNamespace + { + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode, typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); + } +}