diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs new file mode 100644 index 00000000..b1222d4b --- /dev/null +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.DurableTask.Analyzers.Activities; + +/// +/// Analyzer that detects calls to non-existent activities and sub-orchestrations. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class FunctionNotFoundAnalyzer : DiagnosticAnalyzer +{ + /// + /// The diagnostic ID for the diagnostic that reports when an activity call references a function that doesn't exist. + /// + public const string ActivityNotFoundDiagnosticId = "DURABLE2003"; + + /// + /// The diagnostic ID for the diagnostic that reports when a sub-orchestration call references a function that doesn't exist. + /// + public const string SubOrchestrationNotFoundDiagnosticId = "DURABLE2004"; + + static readonly LocalizableString ActivityNotFoundTitle = new LocalizableResourceString(nameof(Resources.ActivityNotFoundAnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString ActivityNotFoundMessageFormat = new LocalizableResourceString(nameof(Resources.ActivityNotFoundAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + + static readonly LocalizableString SubOrchestrationNotFoundTitle = new LocalizableResourceString(nameof(Resources.SubOrchestrationNotFoundAnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString SubOrchestrationNotFoundMessageFormat = new LocalizableResourceString(nameof(Resources.SubOrchestrationNotFoundAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + + static readonly DiagnosticDescriptor ActivityNotFoundRule = new( + ActivityNotFoundDiagnosticId, + ActivityNotFoundTitle, + ActivityNotFoundMessageFormat, + AnalyzersCategories.Activity, + DiagnosticSeverity.Warning, + customTags: [WellKnownDiagnosticTags.CompilationEnd], + isEnabledByDefault: true); + + static readonly DiagnosticDescriptor SubOrchestrationNotFoundRule = new( + SubOrchestrationNotFoundDiagnosticId, + SubOrchestrationNotFoundTitle, + SubOrchestrationNotFoundMessageFormat, + AnalyzersCategories.Orchestration, + DiagnosticSeverity.Warning, + customTags: [WellKnownDiagnosticTags.CompilationEnd], + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => [ActivityNotFoundRule, SubOrchestrationNotFoundRule]; + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(context => + { + KnownTypeSymbols knownSymbols = new(context.Compilation); + + if (knownSymbols.TaskOrchestrationContext == null || + knownSymbols.Task == null || knownSymbols.TaskT == null) + { + // Core symbols not available in this compilation, skip analysis + return; + } + + // Activity-related symbols (may be null if activities aren't used) + IMethodSymbol? taskActivityRunAsync = knownSymbols.TaskActivityBase?.GetMembers("RunAsync").OfType().SingleOrDefault(); + + // Search for Activity and Sub-Orchestrator invocations + ConcurrentBag activityInvocations = []; + ConcurrentBag subOrchestrationInvocations = []; + + context.RegisterOperationAction( + ctx => + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + if (ctx.Operation is not IInvocationOperation invocationOperation) + { + return; + } + + IMethodSymbol targetMethod = invocationOperation.TargetMethod; + + // Check for CallActivityAsync + if (targetMethod.IsEqualTo(knownSymbols.TaskOrchestrationContext, "CallActivityAsync")) + { + string? activityName = ExtractFunctionName(invocationOperation, "name", ctx); + if (activityName != null) + { + activityInvocations.Add(new FunctionInvocation(activityName, invocationOperation.Syntax)); + } + } + + // Check for CallSubOrchestratorAsync + if (targetMethod.IsEqualTo(knownSymbols.TaskOrchestrationContext, "CallSubOrchestratorAsync")) + { + string? orchestratorName = ExtractFunctionName(invocationOperation, "orchestratorName", ctx); + if (orchestratorName != null) + { + subOrchestrationInvocations.Add(new FunctionInvocation(orchestratorName, invocationOperation.Syntax)); + } + } + }, + OperationKind.Invocation); + + // Search for Activity definitions + ConcurrentBag activityNames = []; + ConcurrentBag orchestratorNames = []; + + // Search for Durable Functions Activities and Orchestrators definitions (via [Function] attribute) + context.RegisterSymbolAction( + ctx => + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + if (ctx.Symbol is not IMethodSymbol methodSymbol) + { + return; + } + + // Check for Activity defined via [ActivityTrigger] + if (knownSymbols.ActivityTriggerAttribute != null && + methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.ActivityTriggerAttribute) && + knownSymbols.FunctionNameAttribute != null && + methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out string functionName)) + { + activityNames.Add(functionName); + } + + // Check for Orchestrator defined via [OrchestrationTrigger] + if (knownSymbols.FunctionOrchestrationAttribute != null && + methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute) && + knownSymbols.FunctionNameAttribute != null && + methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out string orchestratorFunctionName)) + { + orchestratorNames.Add(orchestratorFunctionName); + } + }, + SymbolKind.Method); + + // Search for TaskActivity definitions (class-based syntax) + context.RegisterSyntaxNodeAction( + ctx => + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + if (ctx.ContainingSymbol is not INamedTypeSymbol classSymbol) + { + return; + } + + if (classSymbol.IsAbstract) + { + return; + } + + // Check for TaskActivity derived classes + if (knownSymbols.TaskActivityBase != null && taskActivityRunAsync != null && + ClassOverridesMethod(classSymbol, taskActivityRunAsync)) + { + activityNames.Add(classSymbol.Name); + } + + // Check for ITaskOrchestrator implementations (class-based orchestrators) + if (knownSymbols.TaskOrchestratorInterface != null && + classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, knownSymbols.TaskOrchestratorInterface))) + { + orchestratorNames.Add(classSymbol.Name); + } + }, + SyntaxKind.ClassDeclaration); + + // Search for Func/Action activities directly registered through DurableTaskRegistry + context.RegisterOperationAction( + ctx => + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + if (ctx.Operation is not IInvocationOperation invocation) + { + return; + } + + if (knownSymbols.DurableTaskRegistry == null || + !SymbolEqualityComparer.Default.Equals(invocation.Type, knownSymbols.DurableTaskRegistry)) + { + return; + } + + // Handle AddActivityFunc registrations + if (invocation.TargetMethod.Name == "AddActivityFunc") + { + string? name = ExtractFunctionName(invocation, "name", ctx); + if (name != null) + { + activityNames.Add(name); + } + } + + // Handle AddOrchestratorFunc registrations + if (invocation.TargetMethod.Name == "AddOrchestratorFunc") + { + string? name = ExtractFunctionName(invocation, "name", ctx); + if (name != null) + { + orchestratorNames.Add(name); + } + } + }, + OperationKind.Invocation); + + // At the end of the compilation, we correlate the invocations with the definitions + context.RegisterCompilationEndAction(ctx => + { + // Create lookup sets for faster searching + HashSet definedActivities = new(activityNames); + HashSet definedOrchestrators = new(orchestratorNames); + + // Report diagnostics for activities not found + foreach (FunctionInvocation invocation in activityInvocations.Where(i => !definedActivities.Contains(i.Name))) + { + Diagnostic diagnostic = RoslynExtensions.BuildDiagnostic( + ActivityNotFoundRule, invocation.InvocationSyntaxNode, invocation.Name); + ctx.ReportDiagnostic(diagnostic); + } + + // Report diagnostics for sub-orchestrators not found + foreach (FunctionInvocation invocation in subOrchestrationInvocations.Where(i => !definedOrchestrators.Contains(i.Name))) + { + Diagnostic diagnostic = RoslynExtensions.BuildDiagnostic( + SubOrchestrationNotFoundRule, invocation.InvocationSyntaxNode, invocation.Name); + ctx.ReportDiagnostic(diagnostic); + } + }); + }); + } + + static string? ExtractFunctionName(IInvocationOperation invocationOperation, string parameterName, OperationAnalysisContext ctx) + { + IArgumentOperation? nameArgumentOperation = invocationOperation.Arguments.SingleOrDefault(a => a.Parameter?.Name == parameterName); + if (nameArgumentOperation == null) + { + return null; + } + + SemanticModel? semanticModel = ctx.Operation.SemanticModel; + if (semanticModel == null) + { + return null; + } + + // extracts the constant value from the argument (e.g.: it can be a nameof, string literal or const field) + Optional constant = semanticModel.GetConstantValue(nameArgumentOperation.Value.Syntax); + if (!constant.HasValue) + { + // not a constant value, we cannot correlate this invocation to an existent function in compile time + return null; + } + + return constant.Value?.ToString(); + } + + static bool ClassOverridesMethod(INamedTypeSymbol classSymbol, IMethodSymbol methodToFind) + { + INamedTypeSymbol? baseType = classSymbol; + while (baseType != null) + { + if (baseType.GetMembers().OfType() + .Any(method => SymbolEqualityComparer.Default.Equals(method.OverriddenMethod?.OriginalDefinition, methodToFind))) + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } + + readonly struct FunctionInvocation(string name, SyntaxNode invocationSyntaxNode) + { + public string Name { get; } = name; + + public SyntaxNode InvocationSyntaxNode { get; } = invocationSyntaxNode; + } +} diff --git a/src/Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/AnalyzerReleases.Unshipped.md index 7d6ac616..150fb735 100644 --- a/src/Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/AnalyzerReleases.Unshipped.md @@ -1,2 +1,9 @@ ; Unshipped analyzer release -; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md \ No newline at end of file +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +DURABLE2003 | Activity | Warning | **FunctionNotFoundAnalyzer**: Warns when an activity function call references a name that does not match any defined activity in the compilation. +DURABLE2004 | Orchestration | Warning | **FunctionNotFoundAnalyzer**: Warns when a sub-orchestration call references a name that does not match any defined orchestrator in the compilation. \ No newline at end of file diff --git a/src/Analyzers/Resources.resx b/src/Analyzers/Resources.resx index 5f8d5219..ee14969a 100644 --- a/src/Analyzers/Resources.resx +++ b/src/Analyzers/Resources.resx @@ -198,4 +198,16 @@ Use '{0}' instead of '{1}' + + The activity function '{0}' was not found in the current compilation. Ensure the activity is defined and the name matches exactly. + + + Activity function not found + + + The sub-orchestration '{0}' was not found in the current compilation. Ensure the orchestrator is defined and the name matches exactly. + + + Sub-orchestration not found + \ No newline at end of file diff --git a/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs b/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs new file mode 100644 index 00000000..7dff2e38 --- /dev/null +++ b/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs @@ -0,0 +1,374 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis.Testing; +using Microsoft.DurableTask.Analyzers.Activities; +using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; + +namespace Microsoft.DurableTask.Analyzers.Tests.Activities; + +public class FunctionNotFoundAnalyzerTests +{ + // ==================== Activity Tests ==================== + + [Fact] + public async Task DurableFunctionActivityInvocationWithMatchingActivity_NoDiagnostic() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await context.CallActivityAsync(nameof(SayHello), ""Tokyo""); +} + +[Function(nameof(SayHello))] +void SayHello([ActivityTrigger] string name) +{ +} +"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task DurableFunctionActivityInvocationWithMismatchedName_ReportsDiagnostic() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await {|#0:context.CallActivityAsync(""SayHallo"", ""Tokyo"")|}; +} + +[Function(nameof(SayHello))] +void SayHello([ActivityTrigger] string name) +{ +} +"); + DiagnosticResult expected = BuildActivityNotFoundDiagnostic().WithLocation(0).WithArguments("SayHallo"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + [Fact] + public async Task DurableFunctionActivityInvocationWithNonExistentActivity_ReportsDiagnostic() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await {|#0:context.CallActivityAsync(""NonExistentActivity"", ""Tokyo"")|}; +} +"); + DiagnosticResult expected = BuildActivityNotFoundDiagnostic().WithLocation(0).WithArguments("NonExistentActivity"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + [Fact] + public async Task TaskActivityInvocationWithMatchingClassBasedActivity_NoDiagnostic() + { + // Arrange + string code = Wrapper.WrapTaskOrchestrator(@" +public class Caller { + async Task Method(TaskOrchestrationContext context) + { + await context.CallActivityAsync(nameof(MyActivity), ""Tokyo""); + } +} + +public class MyActivity : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string cityName) + { + return Task.FromResult(cityName); + } +} +"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task TaskActivityInvocationWithMismatchedName_ReportsDiagnostic() + { + // Arrange + string code = Wrapper.WrapTaskOrchestrator(@" +public class Caller { + async Task Method(TaskOrchestrationContext context) + { + await {|#0:context.CallActivityAsync(""MyActiviti"", ""Tokyo"")|}; + } +} + +public class MyActivity : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string cityName) + { + return Task.FromResult(cityName); + } +} +"); + DiagnosticResult expected = BuildActivityNotFoundDiagnostic().WithLocation(0).WithArguments("MyActiviti"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + [Fact] + public async Task LambdaActivityInvocationWithMatchingActivity_NoDiagnostic() + { + // Arrange + string code = Wrapper.WrapFuncOrchestrator(@" +tasks.AddOrchestratorFunc(""HelloSequence"", async context => + await context.CallActivityAsync(""SayHello"", ""Tokyo"")); + +tasks.AddActivityFunc(""SayHello"", (context, city) => { }); +"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task LambdaActivityInvocationWithMismatchedName_ReportsDiagnostic() + { + // Arrange + string code = Wrapper.WrapFuncOrchestrator(@" +tasks.AddOrchestratorFunc(""HelloSequence"", async context => + await {|#0:context.CallActivityAsync(""SayHallo"", ""Tokyo"")|}); + +tasks.AddActivityFunc(""SayHello"", (context, city) => { }); +"); + DiagnosticResult expected = BuildActivityNotFoundDiagnostic().WithLocation(0).WithArguments("SayHallo"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + [Fact] + public async Task ActivityInvocationWithConstVariableName_ReportsDiagnostic() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +const string activityName = ""WrongActivityName""; + +async Task Method(TaskOrchestrationContext context) +{ + await {|#0:context.CallActivityAsync(activityName, ""Tokyo"")|}; +} + +[Function(nameof(SayHello))] +void SayHello([ActivityTrigger] string name) +{ +} +"); + DiagnosticResult expected = BuildActivityNotFoundDiagnostic().WithLocation(0).WithArguments("WrongActivityName"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + [Fact] + public async Task ActivityInvocationWithNonConstVariable_NoDiagnostic() + { + // Arrange - When using a non-const variable, we cannot determine the value at compile-time + // so no diagnostic is reported (to avoid false positives) + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + string activityName = ""NonExistentActivity""; + await context.CallActivityAsync(activityName, ""Tokyo""); +} +"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + // ==================== Sub-Orchestration Tests ==================== + + [Fact] + public async Task SubOrchestrationInvocationWithMatchingOrchestration_NoDiagnostic() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await context.CallSubOrchestratorAsync(nameof(ChildOrchestration), ""input""); +} + +[Function(nameof(ChildOrchestration))] +async Task ChildOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + await Task.CompletedTask; +} +"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task SubOrchestrationInvocationWithMismatchedName_ReportsDiagnostic() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await {|#0:context.CallSubOrchestratorAsync(""ChildOrchestration_WrongName"", ""input"")|}; +} + +[Function(nameof(ChildOrchestration))] +async Task ChildOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + await Task.CompletedTask; +} +"); + DiagnosticResult expected = BuildSubOrchestrationNotFoundDiagnostic().WithLocation(0).WithArguments("ChildOrchestration_WrongName"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + [Fact] + public async Task SubOrchestrationInvocationWithNonExistentOrchestrator_ReportsDiagnostic() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await {|#0:context.CallSubOrchestratorAsync(""NonExistentOrchestrator"", ""input"")|}; +} +"); + DiagnosticResult expected = BuildSubOrchestrationNotFoundDiagnostic().WithLocation(0).WithArguments("NonExistentOrchestrator"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + [Fact] + public async Task SubOrchestrationInvocationWithClassBasedOrchestrator_NoDiagnostic() + { + // Arrange + string code = Wrapper.WrapTaskOrchestrator(@" +public class ParentOrchestration : TaskOrchestrator +{ + public override async Task RunAsync(TaskOrchestrationContext context, string input) + { + await context.CallSubOrchestratorAsync(nameof(ChildOrchestration), ""input""); + return ""done""; + } +} + +public class ChildOrchestration : TaskOrchestrator +{ + public override Task RunAsync(TaskOrchestrationContext context, string input) + { + return Task.FromResult(input); + } +} +"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task SubOrchestrationInvocationWithLambdaOrchestrator_NoDiagnostic() + { + // Arrange + string code = Wrapper.WrapFuncOrchestrator(@" +tasks.AddOrchestratorFunc(""ParentOrchestration"", async context => + await context.CallSubOrchestratorAsync(""ChildOrchestration"", ""input"")); + +tasks.AddOrchestratorFunc(""ChildOrchestration"", context => Task.CompletedTask); +"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task SubOrchestrationInvocationWithMismatchedLambdaOrchestrator_ReportsDiagnostic() + { + // Arrange + string code = Wrapper.WrapFuncOrchestrator(@" +tasks.AddOrchestratorFunc(""ParentOrchestration"", async context => + await {|#0:context.CallSubOrchestratorAsync(""ChildOrchestration_WrongName"", ""input"")|}); + +tasks.AddOrchestratorFunc(""ChildOrchestration"", context => Task.CompletedTask); +"); + DiagnosticResult expected = BuildSubOrchestrationNotFoundDiagnostic().WithLocation(0).WithArguments("ChildOrchestration_WrongName"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + [Fact] + public async Task SubOrchestrationInvocationWithTypedResult_NoDiagnostic() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + int result = await context.CallSubOrchestratorAsync(nameof(ChildOrchestration), ""input""); +} + +[Function(nameof(ChildOrchestration))] +async Task ChildOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return 42; +} +"); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task MultipleInvocationsWithSomeMissingFunctions_ReportsMultipleDiagnostics() + { + // Arrange + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await context.CallActivityAsync(nameof(ExistingActivity), ""input""); + await {|#0:context.CallActivityAsync(""MissingActivity"", ""input"")|}; + await context.CallSubOrchestratorAsync(nameof(ExistingOrchestrator), ""input""); + await {|#1:context.CallSubOrchestratorAsync(""MissingOrchestrator"", ""input"")|}; +} + +[Function(nameof(ExistingActivity))] +void ExistingActivity([ActivityTrigger] string name) { } + +[Function(nameof(ExistingOrchestrator))] +async Task ExistingOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context) +{ + await Task.CompletedTask; +} +"); + DiagnosticResult[] expected = + [ + BuildActivityNotFoundDiagnostic().WithLocation(0).WithArguments("MissingActivity"), + BuildSubOrchestrationNotFoundDiagnostic().WithLocation(1).WithArguments("MissingOrchestrator") + ]; + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + static DiagnosticResult BuildActivityNotFoundDiagnostic() + { + return VerifyCS.Diagnostic(FunctionNotFoundAnalyzer.ActivityNotFoundDiagnosticId); + } + + static DiagnosticResult BuildSubOrchestrationNotFoundDiagnostic() + { + return VerifyCS.Diagnostic(FunctionNotFoundAnalyzer.SubOrchestrationNotFoundDiagnosticId); + } +}