Skip to content

Commit 95e7488

Browse files
CopilotYunchuWang
andcommitted
Merge main branch to resolve conflicts
Co-authored-by: YunchuWang <[email protected]>
1 parent 405b814 commit 95e7488

File tree

13 files changed

+908
-16
lines changed

13 files changed

+908
-16
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ To get started, add the [Microsoft.Azure.Functions.Worker.Extensions.DurableTask
3535
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.2.2" />
3636
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
3737
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.7.0" OutputItemType="Analyzer" />
38-
<PackageReference Include="Microsoft.DurableTask.Generators" Version="1.0.0-preview.1" OutputItemType="Analyzer" />
38+
<PackageReference Include="Microsoft.DurableTask.Generators" Version="1.0.0" OutputItemType="Analyzer" />
3939
</ItemGroup>
4040
```
4141

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Concurrent;
5+
using System.Collections.Immutable;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using Microsoft.CodeAnalysis.Operations;
10+
11+
namespace Microsoft.DurableTask.Analyzers.Activities;
12+
13+
/// <summary>
14+
/// Analyzer that detects calls to non-existent activities and sub-orchestrations.
15+
/// </summary>
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public sealed class FunctionNotFoundAnalyzer : DiagnosticAnalyzer
18+
{
19+
/// <summary>
20+
/// The diagnostic ID for the diagnostic that reports when an activity call references a function that doesn't exist.
21+
/// </summary>
22+
public const string ActivityNotFoundDiagnosticId = "DURABLE2003";
23+
24+
/// <summary>
25+
/// The diagnostic ID for the diagnostic that reports when a sub-orchestration call references a function that doesn't exist.
26+
/// </summary>
27+
public const string SubOrchestrationNotFoundDiagnosticId = "DURABLE2004";
28+
29+
static readonly LocalizableString ActivityNotFoundTitle = new LocalizableResourceString(nameof(Resources.ActivityNotFoundAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
30+
static readonly LocalizableString ActivityNotFoundMessageFormat = new LocalizableResourceString(nameof(Resources.ActivityNotFoundAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
31+
32+
static readonly LocalizableString SubOrchestrationNotFoundTitle = new LocalizableResourceString(nameof(Resources.SubOrchestrationNotFoundAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
33+
static readonly LocalizableString SubOrchestrationNotFoundMessageFormat = new LocalizableResourceString(nameof(Resources.SubOrchestrationNotFoundAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
34+
35+
static readonly DiagnosticDescriptor ActivityNotFoundRule = new(
36+
ActivityNotFoundDiagnosticId,
37+
ActivityNotFoundTitle,
38+
ActivityNotFoundMessageFormat,
39+
AnalyzersCategories.Activity,
40+
DiagnosticSeverity.Warning,
41+
customTags: [WellKnownDiagnosticTags.CompilationEnd],
42+
isEnabledByDefault: true);
43+
44+
static readonly DiagnosticDescriptor SubOrchestrationNotFoundRule = new(
45+
SubOrchestrationNotFoundDiagnosticId,
46+
SubOrchestrationNotFoundTitle,
47+
SubOrchestrationNotFoundMessageFormat,
48+
AnalyzersCategories.Orchestration,
49+
DiagnosticSeverity.Warning,
50+
customTags: [WellKnownDiagnosticTags.CompilationEnd],
51+
isEnabledByDefault: true);
52+
53+
/// <inheritdoc/>
54+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ActivityNotFoundRule, SubOrchestrationNotFoundRule];
55+
56+
/// <inheritdoc/>
57+
public override void Initialize(AnalysisContext context)
58+
{
59+
context.EnableConcurrentExecution();
60+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
61+
62+
context.RegisterCompilationStartAction(context =>
63+
{
64+
KnownTypeSymbols knownSymbols = new(context.Compilation);
65+
66+
if (knownSymbols.TaskOrchestrationContext == null ||
67+
knownSymbols.Task == null || knownSymbols.TaskT == null)
68+
{
69+
// Core symbols not available in this compilation, skip analysis
70+
return;
71+
}
72+
73+
// Activity-related symbols (may be null if activities aren't used)
74+
IMethodSymbol? taskActivityRunAsync = knownSymbols.TaskActivityBase?.GetMembers("RunAsync").OfType<IMethodSymbol>().SingleOrDefault();
75+
76+
// Search for Activity and Sub-Orchestrator invocations
77+
ConcurrentBag<FunctionInvocation> activityInvocations = [];
78+
ConcurrentBag<FunctionInvocation> subOrchestrationInvocations = [];
79+
80+
context.RegisterOperationAction(
81+
ctx =>
82+
{
83+
ctx.CancellationToken.ThrowIfCancellationRequested();
84+
85+
if (ctx.Operation is not IInvocationOperation invocationOperation)
86+
{
87+
return;
88+
}
89+
90+
IMethodSymbol targetMethod = invocationOperation.TargetMethod;
91+
92+
// Check for CallActivityAsync
93+
if (targetMethod.IsEqualTo(knownSymbols.TaskOrchestrationContext, "CallActivityAsync"))
94+
{
95+
string? activityName = ExtractFunctionName(invocationOperation, "name", ctx);
96+
if (activityName != null)
97+
{
98+
activityInvocations.Add(new FunctionInvocation(activityName, invocationOperation.Syntax));
99+
}
100+
}
101+
102+
// Check for CallSubOrchestratorAsync
103+
if (targetMethod.IsEqualTo(knownSymbols.TaskOrchestrationContext, "CallSubOrchestratorAsync"))
104+
{
105+
string? orchestratorName = ExtractFunctionName(invocationOperation, "orchestratorName", ctx);
106+
if (orchestratorName != null)
107+
{
108+
subOrchestrationInvocations.Add(new FunctionInvocation(orchestratorName, invocationOperation.Syntax));
109+
}
110+
}
111+
},
112+
OperationKind.Invocation);
113+
114+
// Search for Activity definitions
115+
ConcurrentBag<string> activityNames = [];
116+
ConcurrentBag<string> orchestratorNames = [];
117+
118+
// Search for Durable Functions Activities and Orchestrators definitions (via [Function] attribute)
119+
context.RegisterSymbolAction(
120+
ctx =>
121+
{
122+
ctx.CancellationToken.ThrowIfCancellationRequested();
123+
124+
if (ctx.Symbol is not IMethodSymbol methodSymbol)
125+
{
126+
return;
127+
}
128+
129+
// Check for Activity defined via [ActivityTrigger]
130+
if (knownSymbols.ActivityTriggerAttribute != null &&
131+
methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.ActivityTriggerAttribute) &&
132+
knownSymbols.FunctionNameAttribute != null &&
133+
methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out string functionName))
134+
{
135+
activityNames.Add(functionName);
136+
}
137+
138+
// Check for Orchestrator defined via [OrchestrationTrigger]
139+
if (knownSymbols.FunctionOrchestrationAttribute != null &&
140+
methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute) &&
141+
knownSymbols.FunctionNameAttribute != null &&
142+
methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out string orchestratorFunctionName))
143+
{
144+
orchestratorNames.Add(orchestratorFunctionName);
145+
}
146+
},
147+
SymbolKind.Method);
148+
149+
// Search for TaskActivity<TInput, TOutput> definitions (class-based syntax)
150+
context.RegisterSyntaxNodeAction(
151+
ctx =>
152+
{
153+
ctx.CancellationToken.ThrowIfCancellationRequested();
154+
155+
if (ctx.ContainingSymbol is not INamedTypeSymbol classSymbol)
156+
{
157+
return;
158+
}
159+
160+
if (classSymbol.IsAbstract)
161+
{
162+
return;
163+
}
164+
165+
// Check for TaskActivity<TInput, TOutput> derived classes
166+
if (knownSymbols.TaskActivityBase != null && taskActivityRunAsync != null &&
167+
ClassOverridesMethod(classSymbol, taskActivityRunAsync))
168+
{
169+
activityNames.Add(classSymbol.Name);
170+
}
171+
172+
// Check for ITaskOrchestrator implementations (class-based orchestrators)
173+
if (knownSymbols.TaskOrchestratorInterface != null &&
174+
classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, knownSymbols.TaskOrchestratorInterface)))
175+
{
176+
orchestratorNames.Add(classSymbol.Name);
177+
}
178+
},
179+
SyntaxKind.ClassDeclaration);
180+
181+
// Search for Func/Action activities directly registered through DurableTaskRegistry
182+
context.RegisterOperationAction(
183+
ctx =>
184+
{
185+
ctx.CancellationToken.ThrowIfCancellationRequested();
186+
187+
if (ctx.Operation is not IInvocationOperation invocation)
188+
{
189+
return;
190+
}
191+
192+
if (knownSymbols.DurableTaskRegistry == null ||
193+
!SymbolEqualityComparer.Default.Equals(invocation.Type, knownSymbols.DurableTaskRegistry))
194+
{
195+
return;
196+
}
197+
198+
// Handle AddActivityFunc registrations
199+
if (invocation.TargetMethod.Name == "AddActivityFunc")
200+
{
201+
string? name = ExtractFunctionName(invocation, "name", ctx);
202+
if (name != null)
203+
{
204+
activityNames.Add(name);
205+
}
206+
}
207+
208+
// Handle AddOrchestratorFunc registrations
209+
if (invocation.TargetMethod.Name == "AddOrchestratorFunc")
210+
{
211+
string? name = ExtractFunctionName(invocation, "name", ctx);
212+
if (name != null)
213+
{
214+
orchestratorNames.Add(name);
215+
}
216+
}
217+
},
218+
OperationKind.Invocation);
219+
220+
// At the end of the compilation, we correlate the invocations with the definitions
221+
context.RegisterCompilationEndAction(ctx =>
222+
{
223+
// Create lookup sets for faster searching
224+
HashSet<string> definedActivities = new(activityNames);
225+
HashSet<string> definedOrchestrators = new(orchestratorNames);
226+
227+
// Report diagnostics for activities not found
228+
foreach (FunctionInvocation invocation in activityInvocations.Where(i => !definedActivities.Contains(i.Name)))
229+
{
230+
Diagnostic diagnostic = RoslynExtensions.BuildDiagnostic(
231+
ActivityNotFoundRule, invocation.InvocationSyntaxNode, invocation.Name);
232+
ctx.ReportDiagnostic(diagnostic);
233+
}
234+
235+
// Report diagnostics for sub-orchestrators not found
236+
foreach (FunctionInvocation invocation in subOrchestrationInvocations.Where(i => !definedOrchestrators.Contains(i.Name)))
237+
{
238+
Diagnostic diagnostic = RoslynExtensions.BuildDiagnostic(
239+
SubOrchestrationNotFoundRule, invocation.InvocationSyntaxNode, invocation.Name);
240+
ctx.ReportDiagnostic(diagnostic);
241+
}
242+
});
243+
});
244+
}
245+
246+
static string? ExtractFunctionName(IInvocationOperation invocationOperation, string parameterName, OperationAnalysisContext ctx)
247+
{
248+
IArgumentOperation? nameArgumentOperation = invocationOperation.Arguments.SingleOrDefault(a => a.Parameter?.Name == parameterName);
249+
if (nameArgumentOperation == null)
250+
{
251+
return null;
252+
}
253+
254+
SemanticModel? semanticModel = ctx.Operation.SemanticModel;
255+
if (semanticModel == null)
256+
{
257+
return null;
258+
}
259+
260+
// extracts the constant value from the argument (e.g.: it can be a nameof, string literal or const field)
261+
Optional<object?> constant = semanticModel.GetConstantValue(nameArgumentOperation.Value.Syntax);
262+
if (!constant.HasValue)
263+
{
264+
// not a constant value, we cannot correlate this invocation to an existent function in compile time
265+
return null;
266+
}
267+
268+
return constant.Value?.ToString();
269+
}
270+
271+
static bool ClassOverridesMethod(INamedTypeSymbol classSymbol, IMethodSymbol methodToFind)
272+
{
273+
INamedTypeSymbol? baseType = classSymbol;
274+
while (baseType != null)
275+
{
276+
if (baseType.GetMembers().OfType<IMethodSymbol>()
277+
.Any(method => SymbolEqualityComparer.Default.Equals(method.OverriddenMethod?.OriginalDefinition, methodToFind)))
278+
{
279+
return true;
280+
}
281+
282+
baseType = baseType.BaseType;
283+
}
284+
285+
return false;
286+
}
287+
288+
readonly struct FunctionInvocation(string name, SyntaxNode invocationSyntaxNode)
289+
{
290+
public string Name { get; } = name;
291+
292+
public SyntaxNode InvocationSyntaxNode { get; } = invocationSyntaxNode;
293+
}
294+
}

src/Analyzers/AnalyzerReleases.Shipped.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
; Shipped analyzer releases
22
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
33

4+
## Release 0.2.0
5+
6+
### New Rules
7+
8+
Rule ID | Category | Severity | Notes
9+
--------|----------|----------|-------
10+
DURABLE2003 | Activity | Warning | **FunctionNotFoundAnalyzer**: Warns when an activity function call references a name that does not match any defined activity in the compilation.
11+
DURABLE2004 | Orchestration | Warning | **FunctionNotFoundAnalyzer**: Warns when a sub-orchestration call references a name that does not match any defined orchestrator in the compilation.
12+
413
## Release 0.1.0
514

615
### New Rules
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
; Unshipped analyzer release
2-
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
2+
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
4+
### New Rules
5+
6+
Rule ID | Category | Severity | Notes
7+
--------|----------|----------|-------

src/Analyzers/Analyzers.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</PropertyGroup>
1212

1313
<PropertyGroup>
14-
<VersionPrefix>0.1.0</VersionPrefix>
14+
<VersionPrefix>0.2.0</VersionPrefix>
1515
<VersionSuffix></VersionSuffix>
1616
<PackageDescription>.NET Analyzers for the Durable Task SDK.</PackageDescription>
1717
<NeutralLanguage>en</NeutralLanguage>

src/Analyzers/Resources.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,16 @@
198198
<data name="UseInsteadFixerTitle" xml:space="preserve">
199199
<value>Use '{0}' instead of '{1}'</value>
200200
</data>
201+
<data name="ActivityNotFoundAnalyzerMessageFormat" xml:space="preserve">
202+
<value>The activity function '{0}' was not found in the current compilation. Ensure the activity is defined and the name matches exactly.</value>
203+
</data>
204+
<data name="ActivityNotFoundAnalyzerTitle" xml:space="preserve">
205+
<value>Activity function not found</value>
206+
</data>
207+
<data name="SubOrchestrationNotFoundAnalyzerMessageFormat" xml:space="preserve">
208+
<value>The sub-orchestration '{0}' was not found in the current compilation. Ensure the orchestrator is defined and the name matches exactly.</value>
209+
</data>
210+
<data name="SubOrchestrationNotFoundAnalyzerTitle" xml:space="preserve">
211+
<value>Sub-orchestration not found</value>
212+
</data>
201213
</root>

0 commit comments

Comments
 (0)