Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Roslyn analyzer for workflows #1440

Open
wants to merge 8 commits into
base: release-1.16
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
14 changes: 14 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
@@ -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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -403,6 +407,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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -477,6 +489,8 @@ 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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
8 changes: 8 additions & 0 deletions src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Release 1.16

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
DAPR1001| Usage | Warning | The workflow class '{0}' is not registered
DAPR1002| Usage | Warning | The workflow activity class '{0}' is not registered
3 changes: 3 additions & 0 deletions src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

43 changes: 43 additions & 0 deletions src/Dapr.Workflow.Analyzers/Dapr.Workflow.Analyzers.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
</ItemGroup>

<PropertyGroup>
<!-- Suppress false-positive error NU5128 when packing analyzers with no lib/ref files. -->
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>

<!-- Suppress generation of symbol package (.snupkg). -->
<IncludeSymbols>false</IncludeSymbols>

<!-- Do not include the generator as a lib dependency -->
<IncludeBuildOutput>false</IncludeBuildOutput>

<!-- Additional NuGet package properties. -->
<Description>This package contains analyzers for interacting with Dapr workflows.</Description>
<PackageTags>$(PackageTags)</PackageTags>
</PropertyGroup>

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;

namespace Dapr.Workflow.Analyzers;

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

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

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

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

if (oldInvocation is null)
return document;

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

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

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

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

if (compilation == null)
return document;

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

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

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

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

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

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

// Extract the parameter name from the lambda expression
var parameterName = optionsLambda?.Parameter.Identifier.Text;

// Create the new workflow registration statement
var registerWorkflowStatement = SyntaxFactory.ParseStatement($"{parameterName}.RegisterActivity<{workflowActivityType}>();");

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

// Add the new registration statement to the options block
var newOptionsBlock = optionsBlock.AddStatements(registerWorkflowStatement);

// Replace the old options block with the new one
var newRoot = targetRoot.ReplaceNode(optionsBlock, newOptionsBlock);

// Format the new root.
newRoot = Formatter.Format(newRoot, document.Project.Solution.Workspace);

return targetDocument.WithSyntaxRoot(newRoot);
}

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