From 50934371a40c3ef38d4adeae01cf0724a0674c6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:03:27 +0000 Subject: [PATCH 01/10] Initial plan From dc9c25e6d3231a01b007d368b35db3994108b31d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:15:18 +0000 Subject: [PATCH 02/10] Fix DURABLE2003 false positives for cross-assembly activities Modified FunctionNotFoundAnalyzer to scan referenced assemblies for activity and orchestrator definitions. Added ScanReferencedAssemblies and ScanNamespaceForFunctions methods to detect functions defined in other assemblies. Added three new tests to verify cross-assembly detection works correctly. Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../Activities/FunctionNotFoundAnalyzer.cs | 111 ++++++++++++++++++ .../FunctionNotFoundAnalyzerTests.cs | 97 +++++++++++++++ 2 files changed, 208 insertions(+) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index 1bf920a4..82100884 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -222,6 +222,17 @@ public override void Initialize(AnalysisContext context) // At the end of the compilation, we correlate the invocations with the definitions context.RegisterCompilationEndAction(ctx => { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + // Scan referenced assemblies for activities and orchestrators + ScanReferencedAssemblies( + ctx.Compilation, + knownSymbols, + taskActivityRunAsync, + activityNames, + orchestratorNames, + ctx.CancellationToken); + // Create lookup sets for faster searching HashSet definedActivities = new(activityNames); HashSet definedOrchestrators = new(orchestratorNames); @@ -287,6 +298,106 @@ static bool ClassOverridesMethod(INamedTypeSymbol classSymbol, IMethodSymbol met return false; } + static void ScanReferencedAssemblies( + Compilation compilation, + KnownTypeSymbols knownSymbols, + IMethodSymbol? taskActivityRunAsync, + ConcurrentBag activityNames, + ConcurrentBag orchestratorNames, + CancellationToken cancellationToken) + { + // Scan all referenced assemblies for activities and orchestrators + foreach (IAssemblySymbol assembly in compilation.References + .Select(r => compilation.GetAssemblyOrModuleSymbol(r)) + .OfType()) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Get all types in the assembly + ScanNamespaceForFunctions( + assembly.GlobalNamespace, + knownSymbols, + taskActivityRunAsync, + activityNames, + orchestratorNames, + cancellationToken); + } + } + + static void ScanNamespaceForFunctions( + INamespaceSymbol namespaceSymbol, + KnownTypeSymbols knownSymbols, + IMethodSymbol? taskActivityRunAsync, + ConcurrentBag activityNames, + ConcurrentBag orchestratorNames, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Scan types in this namespace + foreach (INamedTypeSymbol typeSymbol in namespaceSymbol.GetTypeMembers()) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Check for TaskActivity derived classes + if (knownSymbols.TaskActivityBase != null && + taskActivityRunAsync != null && + !typeSymbol.IsAbstract && + ClassOverridesMethod(typeSymbol, taskActivityRunAsync)) + { + activityNames.Add(typeSymbol.Name); + } + + // Check for ITaskOrchestrator implementations (class-based orchestrators) + if (knownSymbols.TaskOrchestratorInterface != null && + typeSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, knownSymbols.TaskOrchestratorInterface))) + { + orchestratorNames.Add(typeSymbol.Name); + } + + // Check methods for [Function] + [ActivityTrigger] or [OrchestrationTrigger] + foreach (ISymbol member in typeSymbol.GetMembers()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (member is not IMethodSymbol methodSymbol) + { + continue; + } + + // 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); + } + } + } + + // Recursively scan nested namespaces + foreach (INamespaceSymbol nestedNamespace in namespaceSymbol.GetNamespaceMembers()) + { + ScanNamespaceForFunctions( + nestedNamespace, + knownSymbols, + taskActivityRunAsync, + activityNames, + orchestratorNames, + cancellationToken); + } + } + readonly struct FunctionInvocation(string name, SyntaxNode invocationSyntaxNode) { public string Name { get; } = name; diff --git a/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs b/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs index 7dff2e38..406fd8dc 100644 --- a/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs +++ b/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs @@ -362,6 +362,103 @@ async Task ExistingOrchestrator([OrchestrationTrigger] TaskOrchestrationContext await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); } + [Fact] + public async Task ActivityInvocationWithActivityDefinedInReferencedAssembly_NoDiagnostic() + { + // Arrange - Orchestrator in main project + string orchestratorCode = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await context.CallActivityAsync(""SayHello"", ""Tokyo""); +} +"); + + // Activity in referenced assembly (simulated via additional source file with different class name) + string activityCode = @" +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; + +class ActivityFunctions +{ + [Function(""SayHello"")] + void SayHello([ActivityTrigger] string name) + { + } +} +"; + + void configureTest(VerifyCS.Test test) => test.TestState.Sources.Add(activityCode); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(orchestratorCode, configureTest); + } + + [Fact] + public async Task SubOrchestrationInvocationWithOrchestratorDefinedInReferencedAssembly_NoDiagnostic() + { + // Arrange - Parent orchestrator in main project + string parentOrchestratorCode = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await context.CallSubOrchestratorAsync(""ChildOrchestration"", ""input""); +} +"); + + // Child orchestrator in referenced assembly (simulated via additional source file with different class name) + string childOrchestratorCode = @" +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; + +class ChildOrchestrators +{ + [Function(""ChildOrchestration"")] + async Task ChildOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) + { + await Task.CompletedTask; + } +} +"; + + void configureTest(VerifyCS.Test test) => test.TestState.Sources.Add(childOrchestratorCode); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(parentOrchestratorCode, configureTest); + } + + [Fact] + public async Task ClassBasedActivityInvocationWithActivityDefinedInReferencedAssembly_NoDiagnostic() + { + // Arrange - Orchestrator in main project + string orchestratorCode = Wrapper.WrapTaskOrchestrator(@" +public class Caller { + async Task Method(TaskOrchestrationContext context) + { + await context.CallActivityAsync(nameof(MyActivity), ""Tokyo""); + } +} +"); + + // Class-based activity in referenced assembly (simulated via additional source file) + string activityCode = @" +using System.Threading.Tasks; +using Microsoft.DurableTask; + +public class MyActivity : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string cityName) + { + return Task.FromResult(cityName); + } +} +"; + + void configureTest(VerifyCS.Test test) => test.TestState.Sources.Add(activityCode); + + // Act & Assert + await VerifyCS.VerifyDurableTaskAnalyzerAsync(orchestratorCode, configureTest); + } + static DiagnosticResult BuildActivityNotFoundDiagnostic() { return VerifyCS.Diagnostic(FunctionNotFoundAnalyzer.ActivityNotFoundDiagnosticId); From 8b7b23ebbf3f0f2b99856d81d392818a9f89ca83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:19:24 +0000 Subject: [PATCH 03/10] Address code review feedback Add assembly filtering to skip system assemblies for performance. Update test comments to accurately describe that they simulate cross-assembly scenarios using separate source files in the same compilation. Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../Activities/FunctionNotFoundAnalyzer.cs | 17 ++++++++++++++++- .../Activities/FunctionNotFoundAnalyzerTests.cs | 6 +++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index 82100884..367a240b 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -307,9 +307,11 @@ static void ScanReferencedAssemblies( CancellationToken cancellationToken) { // Scan all referenced assemblies for activities and orchestrators + // Skip system assemblies for performance foreach (IAssemblySymbol assembly in compilation.References .Select(r => compilation.GetAssemblyOrModuleSymbol(r)) - .OfType()) + .OfType() + .Where(a => !IsSystemAssembly(a))) { cancellationToken.ThrowIfCancellationRequested(); @@ -324,6 +326,19 @@ static void ScanReferencedAssemblies( } } + static bool IsSystemAssembly(IAssemblySymbol assembly) + { + // Skip well-known system assemblies to improve performance + string assemblyName = assembly.Name; + return assemblyName == "mscorlib" || + assemblyName == "System" || + assemblyName == "netstandard" || + assemblyName.StartsWith("System.", StringComparison.Ordinal) || + assemblyName.StartsWith("Microsoft.CodeAnalysis", StringComparison.Ordinal) || + assemblyName.StartsWith("Microsoft.CSharp", StringComparison.Ordinal) || + assemblyName.StartsWith("Microsoft.VisualBasic", StringComparison.Ordinal); + } + static void ScanNamespaceForFunctions( INamespaceSymbol namespaceSymbol, KnownTypeSymbols knownSymbols, diff --git a/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs b/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs index 406fd8dc..1369031e 100644 --- a/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs +++ b/test/Analyzers.Tests/Activities/FunctionNotFoundAnalyzerTests.cs @@ -373,7 +373,7 @@ async Task Method(TaskOrchestrationContext context) } "); - // Activity in referenced assembly (simulated via additional source file with different class name) + // Activity in a separate source file (simulates cross-assembly scenario) string activityCode = @" using Microsoft.Azure.Functions.Worker; using Microsoft.DurableTask; @@ -404,7 +404,7 @@ async Task Method(TaskOrchestrationContext context) } "); - // Child orchestrator in referenced assembly (simulated via additional source file with different class name) + // Child orchestrator in a separate source file (simulates cross-assembly scenario) string childOrchestratorCode = @" using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker; @@ -439,7 +439,7 @@ async Task Method(TaskOrchestrationContext context) } "); - // Class-based activity in referenced assembly (simulated via additional source file) + // Class-based activity in a separate source file (simulates cross-assembly scenario) string activityCode = @" using System.Threading.Tasks; using Microsoft.DurableTask; From 7343692bcb28ab5b44d26abb71ee91649d4dabe6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:24:04 +0000 Subject: [PATCH 04/10] Refactor code based on code review feedback Extract helper methods IsActivityMethod, IsOrchestratorMethod, and ImplementsInterface to reduce code duplication. Move system assembly names to static readonly collections. Optimize LINQ chain in ScanReferencedAssemblies to use simple foreach loop. Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../Activities/FunctionNotFoundAnalyzer.cs | 124 +++++++++++++----- 1 file changed, 94 insertions(+), 30 deletions(-) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index 367a240b..25218fe8 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -10,6 +10,8 @@ namespace Microsoft.DurableTask.Analyzers.Activities; +/// +/// Analyzer that detects calls to non-existent activities and sub-orchestrations. /// /// Analyzer that detects calls to non-existent activities and sub-orchestrations. /// @@ -26,6 +28,22 @@ public sealed class FunctionNotFoundAnalyzer : DiagnosticAnalyzer /// public const string SubOrchestrationNotFoundDiagnosticId = "DURABLE2004"; + // System assemblies to skip when scanning referenced assemblies for performance + static readonly HashSet SystemAssemblyNames = + [ + "mscorlib", + "System", + "netstandard" + ]; + + static readonly string[] SystemAssemblyPrefixes = + [ + "System.", + "Microsoft.CodeAnalysis", + "Microsoft.CSharp", + "Microsoft.VisualBasic" + ]; + 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)); @@ -129,19 +147,13 @@ public override void Initialize(AnalysisContext context) } // Check for Activity defined via [ActivityTrigger] - if (knownSymbols.ActivityTriggerAttribute != null && - methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.ActivityTriggerAttribute) && - knownSymbols.FunctionNameAttribute != null && - methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out string functionName)) + if (IsActivityMethod(methodSymbol, knownSymbols, 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)) + if (IsOrchestratorMethod(methodSymbol, knownSymbols, out string orchestratorFunctionName)) { orchestratorNames.Add(orchestratorFunctionName); } @@ -173,7 +185,7 @@ public override void Initialize(AnalysisContext context) // Check for ITaskOrchestrator implementations (class-based orchestrators) if (knownSymbols.TaskOrchestratorInterface != null && - classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, knownSymbols.TaskOrchestratorInterface))) + ImplementsInterface(classSymbol, knownSymbols.TaskOrchestratorInterface)) { orchestratorNames.Add(classSymbol.Name); } @@ -281,6 +293,49 @@ public override void Initialize(AnalysisContext context) return constant.Value?.ToString(); } + static bool IsActivityMethod(IMethodSymbol methodSymbol, KnownTypeSymbols knownSymbols, out string functionName) + { + functionName = string.Empty; + + if (knownSymbols.ActivityTriggerAttribute == null || + !methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.ActivityTriggerAttribute)) + { + return false; + } + + if (knownSymbols.FunctionNameAttribute == null || + !methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out functionName)) + { + return false; + } + + return true; + } + + static bool IsOrchestratorMethod(IMethodSymbol methodSymbol, KnownTypeSymbols knownSymbols, out string functionName) + { + functionName = string.Empty; + + if (knownSymbols.FunctionOrchestrationAttribute == null || + !methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute)) + { + return false; + } + + if (knownSymbols.FunctionNameAttribute == null || + !methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out functionName)) + { + return false; + } + + return true; + } + + static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol interfaceSymbol) + { + return typeSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, interfaceSymbol)); + } + static bool ClassOverridesMethod(INamedTypeSymbol classSymbol, IMethodSymbol methodToFind) { INamedTypeSymbol? baseType = classSymbol; @@ -308,14 +363,21 @@ static void ScanReferencedAssemblies( { // Scan all referenced assemblies for activities and orchestrators // Skip system assemblies for performance - foreach (IAssemblySymbol assembly in compilation.References - .Select(r => compilation.GetAssemblyOrModuleSymbol(r)) - .OfType() - .Where(a => !IsSystemAssembly(a))) + foreach (MetadataReference reference in compilation.References) { cancellationToken.ThrowIfCancellationRequested(); - // Get all types in the assembly + if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly) + { + continue; + } + + if (IsSystemAssembly(assembly)) + { + continue; + } + + // Scan this assembly ScanNamespaceForFunctions( assembly.GlobalNamespace, knownSymbols, @@ -330,13 +392,21 @@ static bool IsSystemAssembly(IAssemblySymbol assembly) { // Skip well-known system assemblies to improve performance string assemblyName = assembly.Name; - return assemblyName == "mscorlib" || - assemblyName == "System" || - assemblyName == "netstandard" || - assemblyName.StartsWith("System.", StringComparison.Ordinal) || - assemblyName.StartsWith("Microsoft.CodeAnalysis", StringComparison.Ordinal) || - assemblyName.StartsWith("Microsoft.CSharp", StringComparison.Ordinal) || - assemblyName.StartsWith("Microsoft.VisualBasic", StringComparison.Ordinal); + + if (SystemAssemblyNames.Contains(assemblyName)) + { + return true; + } + + foreach (string prefix in SystemAssemblyPrefixes) + { + if (assemblyName.StartsWith(prefix, StringComparison.Ordinal)) + { + return true; + } + } + + return false; } static void ScanNamespaceForFunctions( @@ -365,7 +435,7 @@ static void ScanNamespaceForFunctions( // Check for ITaskOrchestrator implementations (class-based orchestrators) if (knownSymbols.TaskOrchestratorInterface != null && - typeSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, knownSymbols.TaskOrchestratorInterface))) + ImplementsInterface(typeSymbol, knownSymbols.TaskOrchestratorInterface)) { orchestratorNames.Add(typeSymbol.Name); } @@ -381,19 +451,13 @@ static void ScanNamespaceForFunctions( } // Check for Activity defined via [ActivityTrigger] - if (knownSymbols.ActivityTriggerAttribute != null && - methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.ActivityTriggerAttribute) && - knownSymbols.FunctionNameAttribute != null && - methodSymbol.TryGetSingleValueFromAttribute(knownSymbols.FunctionNameAttribute, out string functionName)) + if (IsActivityMethod(methodSymbol, knownSymbols, 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)) + if (IsOrchestratorMethod(methodSymbol, knownSymbols, out string orchestratorFunctionName)) { orchestratorNames.Add(orchestratorFunctionName); } From b0d9ffdd0afacfa0fd3e9149be68f87f086d686b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:29:33 +0000 Subject: [PATCH 05/10] Fix duplicate XML documentation comment Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index 25218fe8..bd335774 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -10,8 +10,6 @@ namespace Microsoft.DurableTask.Analyzers.Activities; -/// -/// Analyzer that detects calls to non-existent activities and sub-orchestrations. /// /// Analyzer that detects calls to non-existent activities and sub-orchestrations. /// From 171cffaf4f283b4505444669ea07e30135aaa469 Mon Sep 17 00:00:00 2001 From: wangbill Date: Fri, 19 Dec 2025 10:49:24 -0800 Subject: [PATCH 06/10] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index bd335774..7d04c8ab 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -396,12 +396,9 @@ static bool IsSystemAssembly(IAssemblySymbol assembly) return true; } - foreach (string prefix in SystemAssemblyPrefixes) + if (SystemAssemblyPrefixes.Any(prefix => assemblyName.StartsWith(prefix, StringComparison.Ordinal))) { - if (assemblyName.StartsWith(prefix, StringComparison.Ordinal)) - { - return true; - } + return true; } return false; From b1009a61f266f79748c2868ae0c973a4c258c0c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:04:18 +0000 Subject: [PATCH 07/10] Filter assembly scanning to only Durable Task assemblies Add ShouldScanAssembly method that checks if an assembly references Durable Task types (Microsoft.DurableTask.Abstractions, Microsoft.DurableTask.Worker, or Microsoft.Azure.Functions.Worker.Extensions.DurableTask) before scanning. This prevents scanning of transitive dependencies that don't contain activities or orchestrators, improving performance and reducing false positives. Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../Activities/FunctionNotFoundAnalyzer.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index 7d04c8ab..9355eb43 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -360,7 +360,7 @@ static void ScanReferencedAssemblies( CancellationToken cancellationToken) { // Scan all referenced assemblies for activities and orchestrators - // Skip system assemblies for performance + // Skip system assemblies and assemblies without Durable Task references for performance foreach (MetadataReference reference in compilation.References) { cancellationToken.ThrowIfCancellationRequested(); @@ -375,6 +375,11 @@ static void ScanReferencedAssemblies( continue; } + if (!ShouldScanAssembly(assembly)) + { + continue; + } + // Scan this assembly ScanNamespaceForFunctions( assembly.GlobalNamespace, @@ -404,6 +409,24 @@ static bool IsSystemAssembly(IAssemblySymbol assembly) return false; } + static bool ShouldScanAssembly(IAssemblySymbol assembly) + { + // Only scan assemblies that reference Durable Task types + // This filters out transitive dependencies that don't contain activities/orchestrators + foreach (AssemblyIdentity referencedAssembly in assembly.Modules.SelectMany(m => m.ReferencedAssemblies)) + { + string refName = referencedAssembly.Name; + if (refName == "Microsoft.DurableTask.Abstractions" || + refName == "Microsoft.DurableTask.Worker" || + refName == "Microsoft.Azure.Functions.Worker.Extensions.DurableTask") + { + return true; + } + } + + return false; + } + static void ScanNamespaceForFunctions( INamespaceSymbol namespaceSymbol, KnownTypeSymbols knownSymbols, From 7d44132b73c17cc444f526d9058123ec9a7a2a55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:28:01 +0000 Subject: [PATCH 08/10] Remove Microsoft.DurableTask.Abstractions from assembly filter Microsoft.DurableTask.Abstractions is a transitive dependency that users never directly reference. Users reference either Microsoft.DurableTask.Worker (for non-function scenarios) or Microsoft.Azure.Functions.Worker.Extensions.DurableTask (for Azure Functions), both of which transitively bring in the abstractions package. Updated ShouldScanAssembly to only check for packages users actually reference. Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index 9355eb43..3ae03bf8 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -416,8 +416,10 @@ static bool ShouldScanAssembly(IAssemblySymbol assembly) foreach (AssemblyIdentity referencedAssembly in assembly.Modules.SelectMany(m => m.ReferencedAssemblies)) { string refName = referencedAssembly.Name; - if (refName == "Microsoft.DurableTask.Abstractions" || - refName == "Microsoft.DurableTask.Worker" || + // Check for packages that users directly reference when defining activities/orchestrators + // Microsoft.DurableTask.Worker: for non-function scenarios (console apps, etc.) + // Microsoft.Azure.Functions.Worker.Extensions.DurableTask: for Azure Functions scenarios + if (refName == "Microsoft.DurableTask.Worker" || refName == "Microsoft.Azure.Functions.Worker.Extensions.DurableTask") { return true; From e94bcd524ee31a629839c59724b849ddb2fe7a33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 20:59:46 +0000 Subject: [PATCH 09/10] Use semantic lookup instead of assembly references Replace fragile assembly reference checking with semantic lookup using ContainsDurableSymbols. This checks if an assembly contains any Durable Task types (TaskActivity, ITaskOrchestrator, ActivityTriggerAttribute, OrchestrationTriggerAttribute) by examining the symbol tree, which is more robust and maintainable than checking package references. Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../Activities/FunctionNotFoundAnalyzer.cs | 79 ++++++++++++++++--- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index 3ae03bf8..e51d70c0 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -360,7 +360,7 @@ static void ScanReferencedAssemblies( CancellationToken cancellationToken) { // Scan all referenced assemblies for activities and orchestrators - // Skip system assemblies and assemblies without Durable Task references for performance + // Skip system assemblies and assemblies without Durable symbols for performance foreach (MetadataReference reference in compilation.References) { cancellationToken.ThrowIfCancellationRequested(); @@ -375,7 +375,7 @@ static void ScanReferencedAssemblies( continue; } - if (!ShouldScanAssembly(assembly)) + if (!ShouldScanAssembly(assembly, knownSymbols)) { continue; } @@ -409,18 +409,59 @@ static bool IsSystemAssembly(IAssemblySymbol assembly) return false; } - static bool ShouldScanAssembly(IAssemblySymbol assembly) + static bool ShouldScanAssembly(IAssemblySymbol assembly, KnownTypeSymbols knownSymbols) { - // Only scan assemblies that reference Durable Task types - // This filters out transitive dependencies that don't contain activities/orchestrators - foreach (AssemblyIdentity referencedAssembly in assembly.Modules.SelectMany(m => m.ReferencedAssemblies)) + // Check if assembly contains any Durable Task symbols using semantic lookup + // This is more robust than checking assembly references + return ContainsDurableSymbols(assembly.GlobalNamespace, knownSymbols); + } + + static bool ContainsDurableSymbols(INamespaceSymbol namespaceSymbol, KnownTypeSymbols knownSymbols) + { + // Check types in this namespace for Durable Task symbols + foreach (INamedTypeSymbol typeSymbol in namespaceSymbol.GetTypeMembers()) { - string refName = referencedAssembly.Name; - // Check for packages that users directly reference when defining activities/orchestrators - // Microsoft.DurableTask.Worker: for non-function scenarios (console apps, etc.) - // Microsoft.Azure.Functions.Worker.Extensions.DurableTask: for Azure Functions scenarios - if (refName == "Microsoft.DurableTask.Worker" || - refName == "Microsoft.Azure.Functions.Worker.Extensions.DurableTask") + // Check if type derives from TaskActivity + if (knownSymbols.TaskActivityBase != null && DerivesFrom(typeSymbol, knownSymbols.TaskActivityBase)) + { + return true; + } + + // Check if type implements ITaskOrchestrator + if (knownSymbols.TaskOrchestratorInterface != null && + ImplementsInterface(typeSymbol, knownSymbols.TaskOrchestratorInterface)) + { + return true; + } + + // Check methods for Durable Task attributes + foreach (ISymbol member in typeSymbol.GetMembers()) + { + if (member is not IMethodSymbol methodSymbol) + { + continue; + } + + // Check for ActivityTrigger attribute + if (knownSymbols.ActivityTriggerAttribute != null && + methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.ActivityTriggerAttribute)) + { + return true; + } + + // Check for OrchestrationTrigger attribute + if (knownSymbols.FunctionOrchestrationAttribute != null && + methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute)) + { + return true; + } + } + } + + // Recursively check nested namespaces + foreach (INamespaceSymbol nestedNamespace in namespaceSymbol.GetNamespaceMembers()) + { + if (ContainsDurableSymbols(nestedNamespace, knownSymbols)) { return true; } @@ -429,6 +470,20 @@ static bool ShouldScanAssembly(IAssemblySymbol assembly) return false; } + static bool DerivesFrom(INamedTypeSymbol typeSymbol, INamedTypeSymbol baseType) + { + INamedTypeSymbol? current = typeSymbol.BaseType; + while (current != null) + { + if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, baseType)) + { + return true; + } + current = current.BaseType; + } + return false; + } + static void ScanNamespaceForFunctions( INamespaceSymbol namespaceSymbol, KnownTypeSymbols knownSymbols, From 9f8c8ed24d547eeab169d797aeecc92d8fd60dec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:23:23 +0000 Subject: [PATCH 10/10] Optimize assembly scanning to avoid double traversal Removed ShouldScanAssembly and ContainsDurableSymbols methods that caused the namespace tree to be traversed twice (once to check if assembly has Durable symbols, then again to collect function names). Now directly calls ScanNamespaceForFunctions which does all the work in a single pass. IsSystemAssembly already filters out the vast majority of irrelevant assemblies. Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../Activities/FunctionNotFoundAnalyzer.cs | 84 +------------------ 1 file changed, 2 insertions(+), 82 deletions(-) diff --git a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs index e51d70c0..f59758f2 100644 --- a/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs +++ b/src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs @@ -360,7 +360,7 @@ static void ScanReferencedAssemblies( CancellationToken cancellationToken) { // Scan all referenced assemblies for activities and orchestrators - // Skip system assemblies and assemblies without Durable symbols for performance + // Skip system assemblies for performance foreach (MetadataReference reference in compilation.References) { cancellationToken.ThrowIfCancellationRequested(); @@ -375,12 +375,7 @@ static void ScanReferencedAssemblies( continue; } - if (!ShouldScanAssembly(assembly, knownSymbols)) - { - continue; - } - - // Scan this assembly + // Scan this assembly - if it doesn't contain Durable functions, nothing will be added ScanNamespaceForFunctions( assembly.GlobalNamespace, knownSymbols, @@ -409,81 +404,6 @@ static bool IsSystemAssembly(IAssemblySymbol assembly) return false; } - static bool ShouldScanAssembly(IAssemblySymbol assembly, KnownTypeSymbols knownSymbols) - { - // Check if assembly contains any Durable Task symbols using semantic lookup - // This is more robust than checking assembly references - return ContainsDurableSymbols(assembly.GlobalNamespace, knownSymbols); - } - - static bool ContainsDurableSymbols(INamespaceSymbol namespaceSymbol, KnownTypeSymbols knownSymbols) - { - // Check types in this namespace for Durable Task symbols - foreach (INamedTypeSymbol typeSymbol in namespaceSymbol.GetTypeMembers()) - { - // Check if type derives from TaskActivity - if (knownSymbols.TaskActivityBase != null && DerivesFrom(typeSymbol, knownSymbols.TaskActivityBase)) - { - return true; - } - - // Check if type implements ITaskOrchestrator - if (knownSymbols.TaskOrchestratorInterface != null && - ImplementsInterface(typeSymbol, knownSymbols.TaskOrchestratorInterface)) - { - return true; - } - - // Check methods for Durable Task attributes - foreach (ISymbol member in typeSymbol.GetMembers()) - { - if (member is not IMethodSymbol methodSymbol) - { - continue; - } - - // Check for ActivityTrigger attribute - if (knownSymbols.ActivityTriggerAttribute != null && - methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.ActivityTriggerAttribute)) - { - return true; - } - - // Check for OrchestrationTrigger attribute - if (knownSymbols.FunctionOrchestrationAttribute != null && - methodSymbol.ContainsAttributeInAnyMethodArguments(knownSymbols.FunctionOrchestrationAttribute)) - { - return true; - } - } - } - - // Recursively check nested namespaces - foreach (INamespaceSymbol nestedNamespace in namespaceSymbol.GetNamespaceMembers()) - { - if (ContainsDurableSymbols(nestedNamespace, knownSymbols)) - { - return true; - } - } - - return false; - } - - static bool DerivesFrom(INamedTypeSymbol typeSymbol, INamedTypeSymbol baseType) - { - INamedTypeSymbol? current = typeSymbol.BaseType; - while (current != null) - { - if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, baseType)) - { - return true; - } - current = current.BaseType; - } - return false; - } - static void ScanNamespaceForFunctions( INamespaceSymbol namespaceSymbol, KnownTypeSymbols knownSymbols,