From 18f4674c63efcddd29a42414cd1188dc58008b51 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Sun, 16 Nov 2025 20:17:40 -0800 Subject: [PATCH 1/6] initial commit --- ...MatchingInputOutputTypeActivityAnalyzer.cs | 142 +++++++++++++++++- ...ingInputOutputTypeActivityAnalyzerTests.cs | 141 +++++++++++++++++ 2 files changed, 281 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs index d749df24d..553fc76ee 100644 --- a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs +++ b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs @@ -75,6 +75,9 @@ public override void Initialize(AnalysisContext context) IMethodSymbol taskActivityRunAsync = knownSymbols.TaskActivityBase.GetMembers("RunAsync").OfType().Single(); INamedTypeSymbol voidSymbol = context.Compilation.GetSpecialType(SpecialType.System_Void); + // Get common DI types that should not be treated as activity input + INamedTypeSymbol? functionContextSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Azure.Functions.Worker.FunctionContext"); + // Search for Activity invocations ConcurrentBag invocations = []; context.RegisterOperationAction( @@ -161,6 +164,12 @@ public override void Initialize(AnalysisContext context) return; } + // If the parameter is FunctionContext, skip validation for this activity (it's a DI parameter, not real input) + if (functionContextSymbol != null && SymbolEqualityComparer.Default.Equals(inputParam.Type, functionContextSymbol)) + { + return; + } + ITypeSymbol? inputType = inputParam.Type; ITypeSymbol? outputType = methodSymbol.ReturnType; @@ -306,7 +315,8 @@ public override void Initialize(AnalysisContext context) continue; } - if (!SymbolEqualityComparer.Default.Equals(invocation.InputType, activity.InputType)) + // Check input type compatibility + if (!AreTypesCompatible(ctx.Compilation, invocation.InputType, activity.InputType)) { string actual = invocation.InputType?.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat) ?? "none"; string expected = activity.InputType?.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat) ?? "none"; @@ -316,7 +326,8 @@ public override void Initialize(AnalysisContext context) ctx.ReportDiagnostic(diagnostic); } - if (!SymbolEqualityComparer.Default.Equals(invocation.OutputType, activity.OutputType)) + // Check output type compatibility + if (!AreTypesCompatible(ctx.Compilation, activity.OutputType, invocation.OutputType)) { string actual = invocation.OutputType?.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat) ?? "none"; string expected = activity.OutputType?.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat) ?? "none"; @@ -330,6 +341,133 @@ public override void Initialize(AnalysisContext context) }); } + /// + /// Checks if the source type is compatible with (can be assigned to) the target type. + /// This handles polymorphism, interface implementation, inheritance, and collection type compatibility. + /// + static bool AreTypesCompatible(Compilation compilation, ITypeSymbol? sourceType, ITypeSymbol? targetType) + { + // Both null = compatible + if (sourceType == null && targetType == null) + { + return true; + } + + // One is null, the other isn't = not compatible + if (sourceType == null || targetType == null) + { + return false; + } + + // Check if types are exactly equal + if (SymbolEqualityComparer.Default.Equals(sourceType, targetType)) + { + return true; + } + + // Check if source type can be converted to target type (handles inheritance, interface implementation, etc.) + Conversion conversion = compilation.ClassifyConversion(sourceType, targetType); + if (conversion.IsImplicit || conversion.IsIdentity) + { + return true; + } + + // Special handling for collection types since ClassifyConversion doesn't always recognize + // generic interface implementations (e.g., List to IReadOnlyList) + if (IsCollectionTypeCompatible(sourceType, targetType)) + { + return true; + } + + return false; + } + + /// + /// Checks if the source collection type is compatible with the target collection type. + /// Handles common scenarios like List to IReadOnlyList, arrays to IEnumerable, etc. + /// + static bool IsCollectionTypeCompatible(ITypeSymbol sourceType, ITypeSymbol targetType) + { + // Check if source is an array and target is a collection interface + if (sourceType is IArrayTypeSymbol sourceArray && targetType is INamedTypeSymbol targetNamed) + { + return IsArrayCompatibleWithCollectionInterface(sourceArray, targetNamed); + } + + // Both must be generic named types + if (sourceType is not INamedTypeSymbol sourceNamed || targetType is not INamedTypeSymbol targetNamedType) + { + return false; + } + + // Both must be generic types with the same type arguments + if (!sourceNamed.IsGenericType || !targetNamedType.IsGenericType) + { + return false; + } + + if (sourceNamed.TypeArguments.Length != targetNamedType.TypeArguments.Length) + { + return false; + } + + // Check if type arguments are compatible (could be different but compatible types) + for (int i = 0; i < sourceNamed.TypeArguments.Length; i++) + { + if (!SymbolEqualityComparer.Default.Equals(sourceNamed.TypeArguments[i], targetNamedType.TypeArguments[i])) + { + // Type arguments must match exactly for collections (we don't support covariance/contravariance here) + return false; + } + } + + // Check if source type implements or derives from target type + // This handles: List → IReadOnlyList, List → IEnumerable, etc. + return ImplementsInterface(sourceNamed, targetNamedType); + } + + /// + /// Checks if an array type is compatible with a collection interface. + /// + static bool IsArrayCompatibleWithCollectionInterface(IArrayTypeSymbol arrayType, INamedTypeSymbol targetInterface) + { + if (!targetInterface.IsGenericType || targetInterface.TypeArguments.Length != 1) + { + return false; + } + + // Check if array element type matches the generic type argument + if (!SymbolEqualityComparer.Default.Equals(arrayType.ElementType, targetInterface.TypeArguments[0])) + { + return false; + } + + // Array implements: IEnumerable, ICollection, IList, IReadOnlyCollection, IReadOnlyList + string targetName = targetInterface.OriginalDefinition.ToDisplayString(); + return targetName == "System.Collections.Generic.IEnumerable" || + targetName == "System.Collections.Generic.ICollection" || + targetName == "System.Collections.Generic.IList" || + targetName == "System.Collections.Generic.IReadOnlyCollection" || + targetName == "System.Collections.Generic.IReadOnlyList"; + } + + /// + /// Checks if the source type implements the target interface. + /// + static bool ImplementsInterface(INamedTypeSymbol sourceType, INamedTypeSymbol targetInterface) + { + // Check all interfaces implemented by the source type + foreach (INamedTypeSymbol @interface in sourceType.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(@interface.OriginalDefinition, targetInterface.OriginalDefinition)) + { + return true; + } + } + + return false; + } + struct ActivityInvocation { public string Name { get; set; } diff --git a/test/Analyzers.Tests/Activities/MatchingInputOutputTypeActivityAnalyzerTests.cs b/test/Analyzers.Tests/Activities/MatchingInputOutputTypeActivityAnalyzerTests.cs index 2970f6adf..d10eb6ba6 100644 --- a/test/Analyzers.Tests/Activities/MatchingInputOutputTypeActivityAnalyzerTests.cs +++ b/test/Analyzers.Tests/Activities/MatchingInputOutputTypeActivityAnalyzerTests.cs @@ -406,6 +406,147 @@ async Task Method(TaskOrchestrationContext context) await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); } + [Fact] + // Verifies that when FunctionContext is marked with [ActivityTrigger], + // calling the activity without input produces NO DURABLE2001/DURABLE2002 warnings. + // This fixes the issue where FunctionContext was incorrectly treated as required input. + public async Task DurableFunctionActivityWithFunctionContextAsActivityTrigger_CalledWithoutInput() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + int num = await context.CallActivityAsync(nameof(GetNumber)); +} + +[Function(nameof(GetNumber))] +int GetNumber([ActivityTrigger] FunctionContext context) +{ + return 42; +} +"); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + // Verifies that when FunctionContext is marked with [ActivityTrigger], + // calling the activity WITH input also produces NO DURABLE2001/DURABLE2002 warnings. + // The analyzer completely skips validation for FunctionContext activities. + public async Task DurableFunctionActivityWithFunctionContextAsActivityTrigger_CalledWithInput() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + int num = await context.CallActivityAsync(nameof(GetNumber), ""someInput""); +} + +[Function(nameof(GetNumber))] +int GetNumber([ActivityTrigger] FunctionContext context) +{ + return 42; +} +"); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + // Verifies that polymorphism is supported for input types - NO WARNINGS expected. + // A derived type (Exception) should be assignable to a base type (object). + // This tests that the analyzer uses type compatibility rather than exact type matching. + public async Task DurableFunctionActivityWithPolymorphicInput_DerivedToBase() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + Exception ex = new Exception(""error""); + await context.CallActivityAsync(nameof(LogError), ex); +} + +[Function(nameof(LogError))] +void LogError([ActivityTrigger] object error) +{ +} +"); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + // Verifies that collection type compatibility works: List → IReadOnlyList - NO WARNINGS expected. + // This tests the exact scenario from the issue: passing List to an activity expecting IReadOnlyList. + // Uses TaskActivity<> pattern since it works better with generic collection types. + public async Task TaskActivityWithCollectionPolymorphism_ListToIReadOnlyList() + { + string code = Wrapper.WrapTaskOrchestrator(@" +using System.Collections.Generic; + +public class Caller { + async Task Method(TaskOrchestrationContext context) + { + List numbers = new List { 1, 2, 3, 4, 5 }; + int sum = await context.CallActivityAsync(nameof(SumActivity), numbers); + } +} + +public class SumActivity : TaskActivity, int> +{ + public override Task RunAsync(TaskActivityContext context, IReadOnlyList numbers) + { + return Task.FromResult(42); + } +} +"); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + // Verifies that polymorphism is supported for output types - NO WARNINGS expected. + // When an activity returns string but the caller expects object, no warning should occur. + // This tests covariance - a more specific return type is acceptable. + public async Task DurableFunctionActivityWithPolymorphicOutput_StringToObject() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + object result = await context.CallActivityAsync(nameof(GetValue), ""input""); +} + +[Function(nameof(GetValue))] +string GetValue([ActivityTrigger] string input) +{ + return ""hello""; +} +"); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + // Verifies that truly incompatible types still produce DURABLE2001 warnings. + // Passing string when int is expected should fail - this is not a valid conversion. + // This test ensures the analyzer still catches real type mismatches. + public async Task DurableFunctionActivityWithIncompatibleTypes_ShouldFail() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +async Task Method(TaskOrchestrationContext context) +{ + await {|#0:context.CallActivityAsync(nameof(GetNumber), ""text"")|}; +} + +[Function(nameof(GetNumber))] +int GetNumber([ActivityTrigger] int value) +{ + return value; +} +"); + + DiagnosticResult expected = BuildInputDiagnostic().WithLocation(0).WithArguments("string", "int", "GetNumber"); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + static DiagnosticResult BuildInputDiagnostic() { From 365cec0a9a2becd0efaefcdef61eafe5a8258403 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Thu, 20 Nov 2025 15:04:36 -0800 Subject: [PATCH 2/6] add comment and nullableannotation check --- ...MatchingInputOutputTypeActivityAnalyzer.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs index 553fc76ee..10a5ff5f1 100644 --- a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs +++ b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs @@ -111,6 +111,10 @@ public override void Initialize(AnalysisContext context) string activityName = constant.Value!.ToString(); // Try to extract the input argument from the invocation + // Note: Two cases result in inputType being null: + // 1. No input argument provided: CallActivityAsync("activity") + // 2. Explicit null literal: CallActivityAsync("activity", null) + // Both are treated the same - as passing null to the activity parameter ITypeSymbol? inputType = null; IArgumentOperation? inputArgumentParameter = invocationOperation.Arguments.SingleOrDefault(a => a.Parameter?.Name == "input"); if (inputArgumentParameter != null && inputArgumentParameter.ArgumentKind != ArgumentKind.DefaultValue) @@ -353,9 +357,22 @@ static bool AreTypesCompatible(Compilation compilation, ITypeSymbol? sourceType, return true; } - // One is null, the other isn't = not compatible + // One is null, the other isn't if (sourceType == null || targetType == null) { + // Special case: null literals can be passed to nullable parameters + // This handles both nullable reference types (string?) and nullable value types (int?) + if (sourceType == null && targetType != null) + { + // Check if target is a nullable reference type (NullableAnnotation.Annotated) + // or a nullable value type (Nullable) + if (targetType.NullableAnnotation == NullableAnnotation.Annotated || + targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + return true; + } + } + return false; } From 30c59fb57dedc1282318fa9189a26f3ba9c3412c Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Thu, 20 Nov 2025 15:14:11 -0800 Subject: [PATCH 3/6] update coilot cmment --- .../MatchingInputOutputTypeActivityAnalyzer.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs index 10a5ff5f1..2958cab85 100644 --- a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs +++ b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs @@ -474,15 +474,8 @@ static bool IsArrayCompatibleWithCollectionInterface(IArrayTypeSymbol arrayType, static bool ImplementsInterface(INamedTypeSymbol sourceType, INamedTypeSymbol targetInterface) { // Check all interfaces implemented by the source type - foreach (INamedTypeSymbol @interface in sourceType.AllInterfaces) - { - if (SymbolEqualityComparer.Default.Equals(@interface.OriginalDefinition, targetInterface.OriginalDefinition)) - { - return true; - } - } - - return false; + return sourceType.AllInterfaces.Any(@interface => + SymbolEqualityComparer.Default.Equals(@interface.OriginalDefinition, targetInterface.OriginalDefinition)); } struct ActivityInvocation From a7c322469b1e12490e79840618d5cd393eff1b34 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Fri, 21 Nov 2025 14:36:02 -0800 Subject: [PATCH 4/6] update --- ...MatchingInputOutputTypeActivityAnalyzer.cs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs index 2958cab85..e7f1c0cd9 100644 --- a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs +++ b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs @@ -111,10 +111,6 @@ public override void Initialize(AnalysisContext context) string activityName = constant.Value!.ToString(); // Try to extract the input argument from the invocation - // Note: Two cases result in inputType being null: - // 1. No input argument provided: CallActivityAsync("activity") - // 2. Explicit null literal: CallActivityAsync("activity", null) - // Both are treated the same - as passing null to the activity parameter ITypeSymbol? inputType = null; IArgumentOperation? inputArgumentParameter = invocationOperation.Arguments.SingleOrDefault(a => a.Parameter?.Name == "input"); if (inputArgumentParameter != null && inputArgumentParameter.ArgumentKind != ArgumentKind.DefaultValue) @@ -351,29 +347,27 @@ public override void Initialize(AnalysisContext context) /// static bool AreTypesCompatible(Compilation compilation, ITypeSymbol? sourceType, ITypeSymbol? targetType) { - // Both null = compatible + // Both null = compatible (no input/output on both sides) if (sourceType == null && targetType == null) { return true; } - // One is null, the other isn't - if (sourceType == null || targetType == null) + // Special case: null (no input/output provided) can be passed to explicitly nullable parameters + // This handles nullable value types (int?) and nullable reference types (string?) + if (sourceType == null && targetType != null) { - // Special case: null literals can be passed to nullable parameters - // This handles both nullable reference types (string?) and nullable value types (int?) - if (sourceType == null && targetType != null) + // Check if target is a nullable value type (Nullable) + if (targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) { - // Check if target is a nullable reference type (NullableAnnotation.Annotated) - // or a nullable value type (Nullable) - if (targetType.NullableAnnotation == NullableAnnotation.Annotated || - targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - return true; - } + return true; + } + + // Check if target is a nullable reference type (string?) + if (targetType.NullableAnnotation == NullableAnnotation.Annotated) + { + return true; } - - return false; } // Check if types are exactly equal @@ -474,7 +468,7 @@ static bool IsArrayCompatibleWithCollectionInterface(IArrayTypeSymbol arrayType, static bool ImplementsInterface(INamedTypeSymbol sourceType, INamedTypeSymbol targetInterface) { // Check all interfaces implemented by the source type - return sourceType.AllInterfaces.Any(@interface => + return sourceType.AllInterfaces.Any(@interface => SymbolEqualityComparer.Default.Equals(@interface.OriginalDefinition, targetInterface.OriginalDefinition)); } From 5bad75abccacd17cf599d7d278da8acc8767b1cd Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Fri, 21 Nov 2025 15:02:12 -0800 Subject: [PATCH 5/6] fix --- ...MatchingInputOutputTypeActivityAnalyzer.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs index e7f1c0cd9..ebdaac79a 100644 --- a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs +++ b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs @@ -360,14 +360,23 @@ static bool AreTypesCompatible(Compilation compilation, ITypeSymbol? sourceType, // Check if target is a nullable value type (Nullable) if (targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) { - return true; + return true; } - // Check if target is a nullable reference type (string?) + // Check if target is a nullable reference type (string?) if (targetType.NullableAnnotation == NullableAnnotation.Annotated) { return true; } + + // Not nullable, so null input is incompatible + return false; + } + + // If targetType is null but sourceType is not, they're incompatible + if (targetType == null && sourceType != null) + { + return false; } // Check if types are exactly equal @@ -377,7 +386,8 @@ static bool AreTypesCompatible(Compilation compilation, ITypeSymbol? sourceType, } // Check if source type can be converted to target type (handles inheritance, interface implementation, etc.) - Conversion conversion = compilation.ClassifyConversion(sourceType, targetType); + // At this point, both sourceType and targetType are guaranteed to be non-null + Conversion conversion = compilation.ClassifyConversion(sourceType!, targetType!); if (conversion.IsImplicit || conversion.IsIdentity) { return true; @@ -385,7 +395,8 @@ static bool AreTypesCompatible(Compilation compilation, ITypeSymbol? sourceType, // Special handling for collection types since ClassifyConversion doesn't always recognize // generic interface implementations (e.g., List to IReadOnlyList) - if (IsCollectionTypeCompatible(sourceType, targetType)) + // At this point, both sourceType and targetType are guaranteed to be non-null + if (IsCollectionTypeCompatible(sourceType!, targetType!)) { return true; } From a23195d1c1951e9af1ba15537032115160c83553 Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Tue, 25 Nov 2025 11:15:10 -0700 Subject: [PATCH 6/6] Use Roslyn's comparisons --- ...MatchingInputOutputTypeActivityAnalyzer.cs | 104 +----------------- 1 file changed, 2 insertions(+), 102 deletions(-) diff --git a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs index ebdaac79a..0772565bd 100644 --- a/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs +++ b/src/Analyzers/Activities/MatchingInputOutputTypeActivityAnalyzer.cs @@ -379,108 +379,8 @@ static bool AreTypesCompatible(Compilation compilation, ITypeSymbol? sourceType, return false; } - // Check if types are exactly equal - if (SymbolEqualityComparer.Default.Equals(sourceType, targetType)) - { - return true; - } - - // Check if source type can be converted to target type (handles inheritance, interface implementation, etc.) - // At this point, both sourceType and targetType are guaranteed to be non-null - Conversion conversion = compilation.ClassifyConversion(sourceType!, targetType!); - if (conversion.IsImplicit || conversion.IsIdentity) - { - return true; - } - - // Special handling for collection types since ClassifyConversion doesn't always recognize - // generic interface implementations (e.g., List to IReadOnlyList) - // At this point, both sourceType and targetType are guaranteed to be non-null - if (IsCollectionTypeCompatible(sourceType!, targetType!)) - { - return true; - } - - return false; - } - - /// - /// Checks if the source collection type is compatible with the target collection type. - /// Handles common scenarios like List to IReadOnlyList, arrays to IEnumerable, etc. - /// - static bool IsCollectionTypeCompatible(ITypeSymbol sourceType, ITypeSymbol targetType) - { - // Check if source is an array and target is a collection interface - if (sourceType is IArrayTypeSymbol sourceArray && targetType is INamedTypeSymbol targetNamed) - { - return IsArrayCompatibleWithCollectionInterface(sourceArray, targetNamed); - } - - // Both must be generic named types - if (sourceType is not INamedTypeSymbol sourceNamed || targetType is not INamedTypeSymbol targetNamedType) - { - return false; - } - - // Both must be generic types with the same type arguments - if (!sourceNamed.IsGenericType || !targetNamedType.IsGenericType) - { - return false; - } - - if (sourceNamed.TypeArguments.Length != targetNamedType.TypeArguments.Length) - { - return false; - } - - // Check if type arguments are compatible (could be different but compatible types) - for (int i = 0; i < sourceNamed.TypeArguments.Length; i++) - { - if (!SymbolEqualityComparer.Default.Equals(sourceNamed.TypeArguments[i], targetNamedType.TypeArguments[i])) - { - // Type arguments must match exactly for collections (we don't support covariance/contravariance here) - return false; - } - } - - // Check if source type implements or derives from target type - // This handles: List → IReadOnlyList, List → IEnumerable, etc. - return ImplementsInterface(sourceNamed, targetNamedType); - } - - /// - /// Checks if an array type is compatible with a collection interface. - /// - static bool IsArrayCompatibleWithCollectionInterface(IArrayTypeSymbol arrayType, INamedTypeSymbol targetInterface) - { - if (!targetInterface.IsGenericType || targetInterface.TypeArguments.Length != 1) - { - return false; - } - - // Check if array element type matches the generic type argument - if (!SymbolEqualityComparer.Default.Equals(arrayType.ElementType, targetInterface.TypeArguments[0])) - { - return false; - } - - // Array implements: IEnumerable, ICollection, IList, IReadOnlyCollection, IReadOnlyList - string targetName = targetInterface.OriginalDefinition.ToDisplayString(); - return targetName == "System.Collections.Generic.IEnumerable" || - targetName == "System.Collections.Generic.ICollection" || - targetName == "System.Collections.Generic.IList" || - targetName == "System.Collections.Generic.IReadOnlyCollection" || - targetName == "System.Collections.Generic.IReadOnlyList"; - } - - /// - /// Checks if the source type implements the target interface. - /// - static bool ImplementsInterface(INamedTypeSymbol sourceType, INamedTypeSymbol targetInterface) - { - // Check all interfaces implemented by the source type - return sourceType.AllInterfaces.Any(@interface => - SymbolEqualityComparer.Default.Equals(@interface.OriginalDefinition, targetInterface.OriginalDefinition)); + var conversion = compilation.ClassifyConversion(sourceType!, targetType!); + return conversion.IsImplicit || conversion.IsIdentity; } struct ActivityInvocation