Skip to content

Commit 34435cd

Browse files
CopilotYunchuWang
andcommitted
Document orchestration analysis and method probing behavior
Co-authored-by: YunchuWang <[email protected]>
1 parent 9d25387 commit 34435cd

File tree

1 file changed

+184
-0
lines changed

1 file changed

+184
-0
lines changed

src/Analyzers/CONTRIBUTING.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,190 @@ The following resources are useful to start developing Roslyn Analyzers and unde
4343
- [Roslyn analyzers and code-aware library for ImmutableArrays](https://learn.microsoft.com/en-us/visualstudio/extensibility/roslyn-analyzers-and-code-aware-library-for-immutablearrays?view=vs-2022)
4444
- [Roslyn code samples](https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Samples.md)
4545

46+
### Orchestration Analysis and Method Probing
47+
48+
Many of the orchestration analyzers need to understand which methods in a codebase are orchestration entry points and which methods are invoked by orchestrations (directly or indirectly). This section documents how orchestrations are discovered and how method call chains are analyzed.
49+
50+
#### How Orchestrations Are Discovered
51+
52+
The `OrchestrationAnalyzer<TOrchestrationVisitor>` base class provides a unified framework for discovering orchestrations across three different patterns:
53+
54+
##### 1. Durable Functions Orchestrations
55+
56+
Durable Functions orchestrations are discovered by looking for methods with the following characteristics:
57+
- The method has a parameter decorated with `[OrchestrationTrigger]` attribute from `Microsoft.Azure.Functions.Worker` namespace
58+
- The method or class has a `[Function]` attribute with a function name
59+
60+
**Example:**
61+
```cs
62+
[Function("MyOrchestrator")]
63+
public async Task Run([OrchestrationTrigger] TaskOrchestrationContext context)
64+
{
65+
// orchestration logic
66+
}
67+
```
68+
69+
The analyzer registers a `SyntaxNodeAction` for `SyntaxKind.MethodDeclaration` and checks if the method symbol contains the `OrchestrationTriggerAttribute` in any of its parameters.
70+
71+
##### 2. TaskOrchestrator Orchestrations
72+
73+
TaskOrchestrator orchestrations are discovered by looking for classes that:
74+
- Implement the `ITaskOrchestrator` interface, or
75+
- Inherit from the `TaskOrchestrator<TInput, TOutput>` base class
76+
77+
**Example:**
78+
```cs
79+
public class MyOrchestrator : TaskOrchestrator<string, string>
80+
{
81+
public override Task<string> RunAsync(TaskOrchestrationContext context, string input)
82+
{
83+
// orchestration logic
84+
}
85+
}
86+
```
87+
88+
The analyzer registers a `SyntaxNodeAction` for `SyntaxKind.ClassDeclaration` and checks if the class implements `ITaskOrchestrator`. It then looks for methods that have a `TaskOrchestrationContext` parameter.
89+
90+
##### 3. Func Orchestrations
91+
92+
Func orchestrations are discovered by looking for calls to `DurableTaskRegistry.AddOrchestratorFunc()` extension methods where an orchestration is registered as a lambda or method reference.
93+
94+
**Example:**
95+
```cs
96+
tasks.AddOrchestratorFunc("HelloSequence", context =>
97+
{
98+
// orchestration logic
99+
return "Hello";
100+
});
101+
```
102+
103+
The analyzer registers an `OperationAction` for `OperationKind.Invocation` and checks if:
104+
- The invocation returns a `DurableTaskRegistry` type
105+
- The method name is `AddOrchestratorFunc`
106+
- The `orchestrator` parameter is a delegate (lambda or method reference)
107+
108+
#### How Method Probing Works
109+
110+
The `MethodProbeOrchestrationVisitor` class implements recursive method call analysis to detect violations in methods invoked by orchestrations. This is crucial because non-deterministic code can exist not just in the orchestration entry point, but also in helper methods called by the orchestration.
111+
112+
##### Probing Algorithm
113+
114+
1. **Entry Point**: When an orchestration is discovered, the visitor starts analyzing from the orchestration's root method.
115+
116+
2. **Recursive Traversal**: For each method:
117+
- The method is added to a dictionary that tracks which orchestrations invoke it
118+
- All `InvocationExpressionSyntax` nodes within the method body are examined
119+
- For each invocation, the target method is identified via semantic analysis
120+
- The target method is recursively analyzed with the same orchestration name
121+
122+
3. **Cycle Detection**: The analyzer maintains a `ConcurrentDictionary<IMethodSymbol, ConcurrentBag<string>>` that maps methods to the orchestrations that invoke them. Before analyzing a method for a specific orchestration, it checks if that combination has already been processed to prevent infinite recursion.
123+
124+
4. **Cross-Tree Analysis**: When a called method is defined in a different syntax tree (e.g., in another file or partial class), the analyzer obtains the correct `SemanticModel` for that syntax tree to continue analysis.
125+
126+
##### Probing Capabilities
127+
128+
The method probing can follow:
129+
- **Direct method calls**: `Helper()` where `Helper` is a concrete method
130+
- **Static methods**: `MyClass.StaticHelper()`
131+
- **Instance methods**: `myInstance.Method()`
132+
- **Method chains**: Calls through multiple levels (`Method1()``Method2()``Method3()`)
133+
- **Async methods**: Methods returning `Task` or `ValueTask`
134+
- **Lambda expressions**: Inline lambdas and local functions within orchestrations
135+
- **Method references**: Delegates created from method references
136+
- **Partial classes**: Methods defined across multiple files via partial class declarations
137+
- **Recursive methods**: Protected against infinite loops via cycle detection
138+
139+
##### Probing Limitations
140+
141+
The method probing **cannot** follow method calls in the following scenarios:
142+
143+
1. **Interface Method Calls**: When calling a method through an interface reference, the analyzer cannot determine which implementation will be invoked at runtime.
144+
145+
```cs
146+
// Will NOT be analyzed
147+
IHelper helper = GetHelper(); // implementation unknown
148+
helper.DoSomething(); // analyzer cannot follow this call
149+
```
150+
151+
2. **Abstract Method Calls**: Similar to interfaces, abstract method implementations are not known at analysis time.
152+
153+
```cs
154+
// Will NOT be analyzed
155+
abstract class Base {
156+
public abstract void DoWork();
157+
}
158+
// Analyzer cannot determine which derived class implementation is used
159+
```
160+
161+
3. **Virtual Method Overrides**: When a virtual method is overridden, the analyzer can only see the base implementation, not runtime overrides.
162+
163+
```cs
164+
// May not analyze the correct override
165+
Base instance = new Derived();
166+
instance.VirtualMethod(); // analyzer sees Base.VirtualMethod, not Derived.VirtualMethod
167+
```
168+
169+
4. **External Library Methods**: Methods from external assemblies or NuGet packages where source code is not available cannot be analyzed.
170+
171+
```cs
172+
// Will NOT be analyzed
173+
externalLibrary.Process(); // source not available
174+
```
175+
176+
5. **Reflection-Based Invocations**: Methods invoked via reflection cannot be traced by static analysis.
177+
178+
```cs
179+
// Will NOT be analyzed
180+
typeof(MyClass).GetMethod("DoWork").Invoke(obj, null);
181+
```
182+
183+
6. **Dependency Injection**: When methods are invoked on instances resolved via DI containers, the concrete type is not known at analysis time.
184+
185+
```cs
186+
// Will NOT be analyzed if injected
187+
public MyOrchestrator(IService service)
188+
{
189+
service.Process(); // analyzer cannot determine concrete type
190+
}
191+
```
192+
193+
##### Diagnostic Messages
194+
195+
When a violation is detected in a helper method, the diagnostic message explicitly indicates:
196+
- The **method** where the violation occurs
197+
- The **specific violation** (e.g., `System.DateTime.Now`)
198+
- The **orchestration** that invokes the method
199+
200+
**Example diagnostic:**
201+
```
202+
"The method 'HelperMethod' uses 'System.DateTime.Now' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
203+
```
204+
205+
This makes it clear that the issue is not with `HelperMethod` itself, but with its invocation from an orchestration context.
206+
207+
##### Consistency Across Analyzers
208+
209+
All orchestration analyzers that extend `OrchestrationAnalyzer<TOrchestrationVisitor>` should use the same method probing behavior by deriving their visitor from `MethodProbeOrchestrationVisitor`. This ensures consistent behavior across all orchestration-related diagnostics.
210+
211+
**Example Implementation:**
212+
```cs
213+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
214+
public sealed class MyOrchestrationAnalyzer : OrchestrationAnalyzer<MyOrchestrationVisitor>
215+
{
216+
// Analyzer implementation
217+
218+
public sealed class MyOrchestrationVisitor : MethodProbeOrchestrationVisitor
219+
{
220+
protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode methodSyntax,
221+
IMethodSymbol methodSymbol, string orchestrationName, Action<Diagnostic> reportDiagnostic)
222+
{
223+
// Inspect the method for violations
224+
// This will be called for the orchestration entry point and all methods it invokes
225+
}
226+
}
227+
}
228+
```
229+
46230
## Unit Testing
47231

48232
There is a [test project](../../test/Analyzers.Tests/) that contains several unit tests for Durable Task analyzers.

0 commit comments

Comments
 (0)