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 a Job Analyzer to check if scheduled jobs have corresponding invocation urls #1477

Open
wants to merge 10 commits into
base: release-1.16
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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" />
Expand Down
14 changes: 14 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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.Jobs.Analyzers", "src\Dapr.Jobs.Analyzer\Dapr.Jobs.Analyzers.csproj", "{28B87C37-4B52-400F-B84D-64F134931BDC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzer.Tests", "test\Dapr.Jobs.Analyzer.Test\Dapr.Jobs.Analyzer.Tests.csproj", "{CADEAE45-8981-4723-B641-9C28251C7D3B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -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
{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
{28B87C37-4B52-400F-B84D-64F134931BDC}.Release|Any CPU.Build.0 = Release|Any CPU
{CADEAE45-8981-4723-B641-9C28251C7D3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CADEAE45-8981-4723-B641-9C28251C7D3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CADEAE45-8981-4723-B641-9C28251C7D3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CADEAE45-8981-4723-B641-9C28251C7D3B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -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}
{28B87C37-4B52-400F-B84D-64F134931BDC} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{CADEAE45-8981-4723-B641-9C28251C7D3B} = {DD020B34-460F-455F-8D17-CF4A949F100B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
Expand Down
2 changes: 1 addition & 1 deletion examples/Jobs/JobsSample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------
// Copyright 2024 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.
Expand Down
7 changes: 7 additions & 0 deletions src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Release 1.16

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
DAPR3001| Usage | Warning | Job invocations require the MapDaprScheduledJobHandler be set and configured for each anticipated job on IEndpointRouteBuilder
3 changes: 3 additions & 0 deletions src/Dapr.Jobs.Analyzer/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

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

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<IsPackable>false</IsPackable>
<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>

<!-- Additional NuGet package properties. -->
<Description>This package contains Roslyn analyzers for jobs.</Description>
<PackageTags>$(PackageTags)</PackageTags>
</PropertyGroup>
</Project>
115 changes: 115 additions & 0 deletions src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Dapr.Jobs.Analyzer
{
/// <summary>
/// DaprJobsAnalyzer.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MapDaprScheduledJobHandlerAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor DaprJobHandlerRule = new DiagnosticDescriptor(
id: "DAPR3001",
title: "Ensure Post Mapper handler is present for all the Scheduled Jobs",
messageFormat: "Job invocations require the MapDaprScheduledJobHandler be set and configured for each anticipated job on IEndpointRouteBuilder",
category: "Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true
);

private static readonly string DaprJobsNameSpace = "Dapr.Jobs";
private static readonly string DaprJobScheduleJobAsyncMethod = "ScheduleJobAsync";
private static readonly string MethodNameSpace = "Dapr.Jobs.Extensions";
private static readonly string MapDaprScheduledJobHandlerMethod = "MapDaprScheduledJobHandler";

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(DaprJobHandlerRule); } }

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSyntaxNodeAction(AnalyzeJobSchedulerHandler, SyntaxKind.InvocationExpression);
}

private static void AnalyzeJobSchedulerHandler(SyntaxNodeAnalysisContext context)
{
var invocationExpression = (InvocationExpressionSyntax)context.Node;

if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccess)
{
return;
}

if (IsNamespaceAndMethodNameEqual(context, invocationExpression, DaprJobsNameSpace, DaprJobScheduleJobAsyncMethod))
{
var arguments = invocationExpression.ArgumentList.Arguments;
if (arguments.Count > 0 && arguments[0].Expression is LiteralExpressionSyntax literal)
{
string jobName = literal.Token.ValueText;

// Now, we will check for a corresponding endpoint route.
CheckForEndpointRoute(context, jobName);
}
}
}

private static void CheckForEndpointRoute(SyntaxNodeAnalysisContext context, string jobName)
{
var root = context.SemanticModel.SyntaxTree.GetRoot();

// Search for MapPost with the corresponding route
var mapDaprScheduledJobHandlersCount = root.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Where(invocation => IsNamespaceAndMethodNameEqual(context, invocation, MethodNameSpace, MapDaprScheduledJobHandlerMethod))
.Count();

if (mapDaprScheduledJobHandlersCount > 0)
{
return;
}

// If no matching route was found, report a diagnostic
var diagnostic = Diagnostic.Create(DaprJobHandlerRule, default, jobName);
context.ReportDiagnostic(diagnostic);
}

/// <summary>
/// Determines whether a given method invocation matches a specified method name
/// within a given namespace.
///
/// For eg: MapDaprScheduledJobHandler (methodname) from the "Dapr.Jobs.Extension" (symbolNamespace) is being called.
/// </summary>
/// <param name="context">The syntax analysis context providing semantic information.</param>
/// <param name="invocation">The invocation expression to analyze.</param>
/// <param name="symbolNamespace">The expected namespace of the method.</param>
/// <param name="methodName">The expected method name.</param>
/// <returns>
/// <c>true</c> if the method belongs to the specified namespace and has the expected name;
/// otherwise, <c>false</c>.
/// </returns>
private static bool IsNamespaceAndMethodNameEqual(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation, string symbolNamespace, string methodName)
{
var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation.Expression);
if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
{
return false;
}

// Check if the receiver is of type DaprJobsClient
if (methodSymbol?.Name == methodName &&
methodSymbol.ContainingNamespace.ToDisplayString() == symbolNamespace)
{
return true;
}

return false;
}
}
}
35 changes: 35 additions & 0 deletions test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzer.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Dapr.Jobs\Dapr.Jobs.csproj" />
<ProjectReference Include="..\..\src\Dapr.Jobs.Analyzer\Dapr.Jobs.Analyzers.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
Loading