Skip to content

Commit 9e9b39d

Browse files
siri-varmasvegiraju-microsoftWhitWaldo
authored
Adds an analyzer to validate present configured mapping endpoint when jobs are scheduled (#1477)
* Add Analyzer * Refactored for consistent naming. Updated to use Resources instead of constant strings and to share values via internal values where possible. Added location to job names that trigger diagnostic and updated verbiage of formatted value. Co-authored-by: Siri Varma Vegiraju <[email protected]> Co-authored-by: Whit Waldo <[email protected]>
1 parent 76bfed2 commit 9e9b39d

11 files changed

+613
-1
lines changed

all.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs
155155
EndProject
156156
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}"
157157
EndProject
158+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzers", "src\Dapr.Jobs.Analyzer\Dapr.Jobs.Analyzers.csproj", "{28B87C37-4B52-400F-B84D-64F134931BDC}"
159+
EndProject
160+
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}"
161+
EndProject
158162
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers", "src\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers.csproj", "{E49C822C-E921-48DF-897B-3E603CA596D2}"
159163
EndProject
160164
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Tests", "test\Dapr.Actors.Analyzers.Tests\Dapr.Actors.Analyzers.Tests.csproj", "{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}"
@@ -407,6 +411,14 @@ Global
407411
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
408412
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
409413
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU
414+
{28B87C37-4B52-400F-B84D-64F134931BDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
415+
{28B87C37-4B52-400F-B84D-64F134931BDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
416+
{28B87C37-4B52-400F-B84D-64F134931BDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
417+
{28B87C37-4B52-400F-B84D-64F134931BDC}.Release|Any CPU.Build.0 = Release|Any CPU
418+
{CADEAE45-8981-4723-B641-9C28251C7D3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
419+
{CADEAE45-8981-4723-B641-9C28251C7D3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
420+
{CADEAE45-8981-4723-B641-9C28251C7D3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
421+
{CADEAE45-8981-4723-B641-9C28251C7D3B}.Release|Any CPU.Build.0 = Release|Any CPU
410422
{E49C822C-E921-48DF-897B-3E603CA596D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
411423
{E49C822C-E921-48DF-897B-3E603CA596D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
412424
{E49C822C-E921-48DF-897B-3E603CA596D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -488,6 +500,8 @@ Global
488500
{D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
489501
{9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488}
490502
{E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B}
503+
{28B87C37-4B52-400F-B84D-64F134931BDC} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
504+
{CADEAE45-8981-4723-B641-9C28251C7D3B} = {DD020B34-460F-455F-8D17-CF4A949F100B}
491505
{E49C822C-E921-48DF-897B-3E603CA596D2} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
492506
{A2C0F203-11FF-4B7F-A94F-B9FD873573FE} = {DD020B34-460F-455F-8D17-CF4A949F100B}
493507
EndGlobalSection

examples/Jobs/JobsSample/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ------------------------------------------------------------------------
1+
// ------------------------------------------------------------------------
22
// Copyright 2024 The Dapr Authors
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## Release 1.16
2+
3+
### New Rules
4+
5+
Rule ID | Category | Severity | Notes
6+
--------|----------|----------|--------------------
7+
DAPR3001| Usage | Warning | Job invocations require the MapDaprScheduledJobHandler be set and configured for each anticipated job on IEndpointRouteBuilder
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
; Unshipped analyzer release
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2025 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
using System.Runtime.CompilerServices;
15+
16+
[assembly: InternalsVisibleTo("Dapr.Jobs.Analyzers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<TargetFrameworks></TargetFrameworks>
6+
<IsPackable>false</IsPackable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
10+
<NoWarn>$(NoWarn);RS1038</NoWarn>
11+
<Description>This package contains Roslyn analyers for Dapr.Jobs</Description>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
16+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
17+
<PrivateAssets>all</PrivateAssets>
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
19+
</PackageReference>
20+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<EmbeddedResource Update="Resources.resx">
25+
<Generator>ResXFileCodeGenerator</Generator>
26+
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
27+
</EmbeddedResource>
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<Compile Update="Resources.Designer.cs">
32+
<DesignTime>True</DesignTime>
33+
<AutoGen>True</AutoGen>
34+
<DependentUpon>Resources.resx</DependentUpon>
35+
</Compile>
36+
</ItemGroup>
37+
38+
<PropertyGroup>
39+
<!-- Suppress false-positive error NU5128 when packing analyzers with no lib/ref files. -->
40+
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
41+
42+
<!-- Suppress generation of symbol package (.snupkg). -->
43+
<IncludeSymbols>false</IncludeSymbols>
44+
45+
<!-- Additional NuGet package properties. -->
46+
<Description>This package contains Roslyn analyzers for jobs.</Description>
47+
<PackageTags>$(PackageTags)</PackageTags>
48+
</PropertyGroup>
49+
</Project>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Collections.Immutable;
2+
using System.Resources;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
8+
namespace Dapr.Jobs.Analyzers
9+
{
10+
/// <summary>
11+
/// DaprJobsAnalyzer.
12+
/// </summary>
13+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
14+
public sealed class MapDaprScheduledJobHandlerAnalyzer : DiagnosticAnalyzer
15+
{
16+
internal static readonly DiagnosticDescriptor DaprJobHandlerRule = new (
17+
id: "DAPR3001",
18+
title: new LocalizableResourceString(nameof(Resources.DAPR3001Title), Resources.ResourceManager, typeof(Resources)),
19+
messageFormat: new LocalizableResourceString(nameof(Resources.DAPR3001MessageFormat), Resources.ResourceManager, typeof(Resources)),
20+
category: "Usage",
21+
DiagnosticSeverity.Warning,
22+
isEnabledByDefault: true
23+
);
24+
25+
private const string DaprJobsNameSpace = "Dapr.Jobs";
26+
private const string DaprJobScheduleJobAsyncMethod = "ScheduleJobAsync";
27+
private const string MethodNameSpace = "Dapr.Jobs.Extensions";
28+
private const string MapDaprScheduledJobHandlerMethod = "MapDaprScheduledJobHandler";
29+
30+
/// <inheritdoc/>
31+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [DaprJobHandlerRule];
32+
33+
/// <inheritdoc/>
34+
public override void Initialize(AnalysisContext context)
35+
{
36+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
37+
context.EnableConcurrentExecution();
38+
39+
context.RegisterSyntaxNodeAction(AnalyzeJobSchedulerHandler, SyntaxKind.InvocationExpression);
40+
}
41+
42+
private static void AnalyzeJobSchedulerHandler(SyntaxNodeAnalysisContext context)
43+
{
44+
var invocationExpression = (InvocationExpressionSyntax)context.Node;
45+
46+
if (!IsNamespaceAndMethodNameEqual(context, invocationExpression, DaprJobsNameSpace, DaprJobScheduleJobAsyncMethod))
47+
{
48+
return;
49+
}
50+
51+
var arguments = invocationExpression.ArgumentList.Arguments;
52+
if (arguments.Count > 0 && arguments[0].Expression is LiteralExpressionSyntax literal)
53+
{
54+
string jobName = literal.Token.ValueText;
55+
56+
// Now, we will check for a corresponding endpoint route.
57+
var jobNameLocation = invocationExpression.GetLocation();
58+
CheckForEndpointRoute(context, jobName, jobNameLocation);
59+
}
60+
}
61+
62+
private static void CheckForEndpointRoute(SyntaxNodeAnalysisContext context, string jobName, Location jobNameLocation)
63+
{
64+
var root = context.SemanticModel.SyntaxTree.GetRoot();
65+
66+
// Search for MapPost with the corresponding route
67+
var mapDaprScheduledJobHandlersCount = root
68+
.DescendantNodes()
69+
.OfType<InvocationExpressionSyntax>()
70+
.Count(invocation => IsNamespaceAndMethodNameEqual(context, invocation, MethodNameSpace, MapDaprScheduledJobHandlerMethod));
71+
72+
if (mapDaprScheduledJobHandlersCount > 0)
73+
{
74+
return;
75+
}
76+
77+
// If no matching route was found, report a diagnostic
78+
var diagnostic = Diagnostic.Create(DaprJobHandlerRule, jobNameLocation, jobName);
79+
context.ReportDiagnostic(diagnostic);
80+
}
81+
82+
/// <summary>
83+
/// Determines whether a given method invocation matches a specified method name
84+
/// within a given namespace.
85+
///
86+
/// For eg: MapDaprScheduledJobHandler (methodname) from the "Dapr.Jobs.Extension" (symbolNamespace) is being called.
87+
/// </summary>
88+
/// <param name="context">The syntax analysis context providing semantic information.</param>
89+
/// <param name="invocation">The invocation expression to analyze.</param>
90+
/// <param name="symbolNamespace">The expected namespace of the method.</param>
91+
/// <param name="methodName">The expected method name.</param>
92+
/// <returns>
93+
/// <c>true</c> if the method belongs to the specified namespace and has the expected name;
94+
/// otherwise, <c>false</c>.
95+
/// </returns>
96+
private static bool IsNamespaceAndMethodNameEqual(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation, string symbolNamespace, string methodName)
97+
{
98+
var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation.Expression);
99+
if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
100+
{
101+
return false;
102+
}
103+
104+
// Check if the receiver is of type DaprJobsClient
105+
return methodSymbol.Name == methodName &&
106+
methodSymbol.ContainingNamespace.ToDisplayString() == symbolNamespace;
107+
}
108+
}
109+
}

src/Dapr.Jobs.Analyzer/Resources.Designer.cs

Lines changed: 80 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<root>
4+
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
5+
<xsd:element name="root" msdata:IsDataSet="true">
6+
7+
</xsd:element>
8+
</xsd:schema>
9+
<resheader name="resmimetype">
10+
<value>text/microsoft-resx</value>
11+
</resheader>
12+
<resheader name="version">
13+
<value>1.3</value>
14+
</resheader>
15+
<resheader name="reader">
16+
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
17+
</resheader>
18+
<resheader name="writer">
19+
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
20+
</resheader>
21+
<data name="DAPR3001Title" xml:space="preserve">
22+
<value>Ensure handler endpoint is present for all scheduled Jobs</value>
23+
</data>
24+
<data name="DAPR3001MessageFormat" xml:space="preserve">
25+
<value>Job invocations require the MapDaprScheduledJobHandler be set and configured for job name '{0}' on IEndpointRouteBuilder</value>
26+
</data>
27+
</root>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<Nullable>enable</Nullable>
6+
<IsPackable>false</IsPackable>
7+
<IsTestProject>true</IsTestProject>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" />
12+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" />
13+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
15+
<PackageReference Include="xunit" />
16+
<PackageReference Include="xunit.runner.visualstudio">
17+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
<PackageReference Include="coverlet.collector">
21+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
22+
<PrivateAssets>all</PrivateAssets>
23+
</PackageReference>
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<ProjectReference Include="..\..\src\Dapr.Jobs\Dapr.Jobs.csproj" />
28+
<ProjectReference Include="..\..\src\Dapr.Jobs.Analyzer\Dapr.Jobs.Analyzers.csproj" />
29+
</ItemGroup>
30+
31+
<ItemGroup>
32+
<Using Include="Xunit" />
33+
</ItemGroup>
34+
35+
</Project>

0 commit comments

Comments
 (0)