-
Notifications
You must be signed in to change notification settings - Fork 53
Fix FunctionContext Check and Polymorphic Type Conversions in Activity Analyzer #506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
18f4674
9012dd4
365cec0
30c59fb
ec21a2a
a7c3224
5bad75a
6f57905
050f8b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,9 @@ public override void Initialize(AnalysisContext context) | |
| IMethodSymbol taskActivityRunAsync = knownSymbols.TaskActivityBase.GetMembers("RunAsync").OfType<IMethodSymbol>().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"); | ||
nytian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Search for Activity invocations | ||
| ConcurrentBag<ActivityInvocation> 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; | ||
| } | ||
|
Comment on lines
+167
to
+171
|
||
|
|
||
| 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) | |
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 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. | ||
| /// </summary> | ||
| 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<T> to IReadOnlyList<T>) | ||
| if (IsCollectionTypeCompatible(sourceType, targetType)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if the source collection type is compatible with the target collection type. | ||
| /// Handles common scenarios like List to IReadOnlyList, arrays to IEnumerable, etc. | ||
| /// </summary> | ||
| 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; | ||
| } | ||
| } | ||
|
Comment on lines
+436
to
+444
|
||
|
|
||
| // Check if source type implements or derives from target type | ||
nytian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // This handles: List<T> → IReadOnlyList<T>, List<T> → IEnumerable<T>, etc. | ||
| return ImplementsInterface(sourceNamed, targetNamedType); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if an array type is compatible with a collection interface. | ||
| /// </summary> | ||
| 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<T>, ICollection<T>, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T> | ||
| string targetName = targetInterface.OriginalDefinition.ToDisplayString(); | ||
| return targetName == "System.Collections.Generic.IEnumerable<T>" || | ||
| targetName == "System.Collections.Generic.ICollection<T>" || | ||
| targetName == "System.Collections.Generic.IList<T>" || | ||
| targetName == "System.Collections.Generic.IReadOnlyCollection<T>" || | ||
| targetName == "System.Collections.Generic.IReadOnlyList<T>"; | ||
|
Comment on lines
+467
to
+473
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if the source type implements the target interface. | ||
| /// </summary> | ||
| 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; } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.