@@ -75,6 +75,9 @@ public override void Initialize(AnalysisContext context)
7575 IMethodSymbol taskActivityRunAsync = knownSymbols . TaskActivityBase . GetMembers ( "RunAsync" ) . OfType < IMethodSymbol > ( ) . Single ( ) ;
7676 INamedTypeSymbol voidSymbol = context . Compilation . GetSpecialType ( SpecialType . System_Void ) ;
7777
78+ // Get common DI types that should not be treated as activity input
79+ INamedTypeSymbol ? functionContextSymbol = context . Compilation . GetTypeByMetadataName ( "Microsoft.Azure.Functions.Worker.FunctionContext" ) ;
80+
7881 // Search for Activity invocations
7982 ConcurrentBag < ActivityInvocation > invocations = [ ] ;
8083 context . RegisterOperationAction (
@@ -161,6 +164,12 @@ public override void Initialize(AnalysisContext context)
161164 return ;
162165 }
163166
167+ // If the parameter is FunctionContext, skip validation for this activity (it's a DI parameter, not real input)
168+ if ( functionContextSymbol != null && SymbolEqualityComparer . Default . Equals ( inputParam . Type , functionContextSymbol ) )
169+ {
170+ return ;
171+ }
172+
164173 ITypeSymbol ? inputType = inputParam . Type ;
165174
166175 ITypeSymbol ? outputType = methodSymbol . ReturnType ;
@@ -306,7 +315,8 @@ public override void Initialize(AnalysisContext context)
306315 continue ;
307316 }
308317
309- if ( ! SymbolEqualityComparer . Default . Equals ( invocation . InputType , activity . InputType ) )
318+ // Check input type compatibility
319+ if ( ! AreTypesCompatible ( ctx . Compilation , invocation . InputType , activity . InputType ) )
310320 {
311321 string actual = invocation . InputType ? . ToDisplayString ( SymbolDisplayFormat . CSharpShortErrorMessageFormat ) ?? "none" ;
312322 string expected = activity . InputType ? . ToDisplayString ( SymbolDisplayFormat . CSharpShortErrorMessageFormat ) ?? "none" ;
@@ -316,7 +326,8 @@ public override void Initialize(AnalysisContext context)
316326 ctx . ReportDiagnostic ( diagnostic ) ;
317327 }
318328
319- if ( ! SymbolEqualityComparer . Default . Equals ( invocation . OutputType , activity . OutputType ) )
329+ // Check output type compatibility
330+ if ( ! AreTypesCompatible ( ctx . Compilation , activity . OutputType , invocation . OutputType ) )
320331 {
321332 string actual = invocation . OutputType ? . ToDisplayString ( SymbolDisplayFormat . CSharpShortErrorMessageFormat ) ?? "none" ;
322333 string expected = activity . OutputType ? . ToDisplayString ( SymbolDisplayFormat . CSharpShortErrorMessageFormat ) ?? "none" ;
@@ -330,6 +341,148 @@ public override void Initialize(AnalysisContext context)
330341 } ) ;
331342 }
332343
344+ /// <summary>
345+ /// Checks if the source type is compatible with (can be assigned to) the target type.
346+ /// This handles polymorphism, interface implementation, inheritance, and collection type compatibility.
347+ /// </summary>
348+ static bool AreTypesCompatible ( Compilation compilation , ITypeSymbol ? sourceType , ITypeSymbol ? targetType )
349+ {
350+ // Both null = compatible (no input/output on both sides)
351+ if ( sourceType == null && targetType == null )
352+ {
353+ return true ;
354+ }
355+
356+ // Special case: null (no input/output provided) can be passed to explicitly nullable parameters
357+ // This handles nullable value types (int?) and nullable reference types (string?)
358+ if ( sourceType == null && targetType != null )
359+ {
360+ // Check if target is a nullable value type (Nullable<T>)
361+ if ( targetType . OriginalDefinition . SpecialType == SpecialType . System_Nullable_T )
362+ {
363+ return true ;
364+ }
365+
366+ // Check if target is a nullable reference type (string?)
367+ if ( targetType . NullableAnnotation == NullableAnnotation . Annotated )
368+ {
369+ return true ;
370+ }
371+
372+ // Not nullable, so null input is incompatible
373+ return false ;
374+ }
375+
376+ // If targetType is null but sourceType is not, they're incompatible
377+ if ( targetType == null && sourceType != null )
378+ {
379+ return false ;
380+ }
381+
382+ // Check if types are exactly equal
383+ if ( SymbolEqualityComparer . Default . Equals ( sourceType , targetType ) )
384+ {
385+ return true ;
386+ }
387+
388+ // Check if source type can be converted to target type (handles inheritance, interface implementation, etc.)
389+ // At this point, both sourceType and targetType are guaranteed to be non-null
390+ Conversion conversion = compilation . ClassifyConversion ( sourceType ! , targetType ! ) ;
391+ if ( conversion . IsImplicit || conversion . IsIdentity )
392+ {
393+ return true ;
394+ }
395+
396+ // Special handling for collection types since ClassifyConversion doesn't always recognize
397+ // generic interface implementations (e.g., List<T> to IReadOnlyList<T>)
398+ // At this point, both sourceType and targetType are guaranteed to be non-null
399+ if ( IsCollectionTypeCompatible ( sourceType ! , targetType ! ) )
400+ {
401+ return true ;
402+ }
403+
404+ return false ;
405+ }
406+
407+ /// <summary>
408+ /// Checks if the source collection type is compatible with the target collection type.
409+ /// Handles common scenarios like List to IReadOnlyList, arrays to IEnumerable, etc.
410+ /// </summary>
411+ static bool IsCollectionTypeCompatible ( ITypeSymbol sourceType , ITypeSymbol targetType )
412+ {
413+ // Check if source is an array and target is a collection interface
414+ if ( sourceType is IArrayTypeSymbol sourceArray && targetType is INamedTypeSymbol targetNamed )
415+ {
416+ return IsArrayCompatibleWithCollectionInterface ( sourceArray , targetNamed ) ;
417+ }
418+
419+ // Both must be generic named types
420+ if ( sourceType is not INamedTypeSymbol sourceNamed || targetType is not INamedTypeSymbol targetNamedType )
421+ {
422+ return false ;
423+ }
424+
425+ // Both must be generic types with the same type arguments
426+ if ( ! sourceNamed . IsGenericType || ! targetNamedType . IsGenericType )
427+ {
428+ return false ;
429+ }
430+
431+ if ( sourceNamed . TypeArguments . Length != targetNamedType . TypeArguments . Length )
432+ {
433+ return false ;
434+ }
435+
436+ // Check if type arguments are compatible (could be different but compatible types)
437+ for ( int i = 0 ; i < sourceNamed . TypeArguments . Length ; i ++ )
438+ {
439+ if ( ! SymbolEqualityComparer . Default . Equals ( sourceNamed . TypeArguments [ i ] , targetNamedType . TypeArguments [ i ] ) )
440+ {
441+ // Type arguments must match exactly for collections (we don't support covariance/contravariance here)
442+ return false ;
443+ }
444+ }
445+
446+ // Check if source type implements or derives from target type
447+ // This handles: List<T> → IReadOnlyList<T>, List<T> → IEnumerable<T>, etc.
448+ return ImplementsInterface ( sourceNamed , targetNamedType ) ;
449+ }
450+
451+ /// <summary>
452+ /// Checks if an array type is compatible with a collection interface.
453+ /// </summary>
454+ static bool IsArrayCompatibleWithCollectionInterface ( IArrayTypeSymbol arrayType , INamedTypeSymbol targetInterface )
455+ {
456+ if ( ! targetInterface . IsGenericType || targetInterface . TypeArguments . Length != 1 )
457+ {
458+ return false ;
459+ }
460+
461+ // Check if array element type matches the generic type argument
462+ if ( ! SymbolEqualityComparer . Default . Equals ( arrayType . ElementType , targetInterface . TypeArguments [ 0 ] ) )
463+ {
464+ return false ;
465+ }
466+
467+ // Array implements: IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>
468+ string targetName = targetInterface . OriginalDefinition . ToDisplayString ( ) ;
469+ return targetName == "System.Collections.Generic.IEnumerable<T>" ||
470+ targetName == "System.Collections.Generic.ICollection<T>" ||
471+ targetName == "System.Collections.Generic.IList<T>" ||
472+ targetName == "System.Collections.Generic.IReadOnlyCollection<T>" ||
473+ targetName == "System.Collections.Generic.IReadOnlyList<T>" ;
474+ }
475+
476+ /// <summary>
477+ /// Checks if the source type implements the target interface.
478+ /// </summary>
479+ static bool ImplementsInterface ( INamedTypeSymbol sourceType , INamedTypeSymbol targetInterface )
480+ {
481+ // Check all interfaces implemented by the source type
482+ return sourceType . AllInterfaces . Any ( @interface =>
483+ SymbolEqualityComparer . Default . Equals ( @interface . OriginalDefinition , targetInterface . OriginalDefinition ) ) ;
484+ }
485+
333486 struct ActivityInvocation
334487 {
335488 public string Name { get ; set ; }
0 commit comments