Skip to content

Fix: handle array / pointer / dynamic type arguments in nested constraint chains#62

Open
waynebrantley wants to merge 1 commit into
Dreamescaper:mainfrom
waynebrantley:fix/array-typed-generic-constraint
Open

Fix: handle array / pointer / dynamic type arguments in nested constraint chains#62
waynebrantley wants to merge 1 commit into
Dreamescaper:mainfrom
waynebrantley:fix/array-typed-generic-constraint

Conversation

@waynebrantley

Copy link
Copy Markdown

Summary

MatchedTypeSatisfiesConstraints in DependencyInjectionGenerator.FilterTypes.cs requires every candidate type argument in a nested constraint chain to be an INamedTypeSymbol. When the recursion reaches a constructed generic whose type argument is an array (T[], T[][]), pointer, or dynamic, the pattern-match fails and the whole handler is silently dropped from generation — no diagnostic, no warning.

Repro (fails on main)

using ServiceScan.SourceGenerator;

namespace Repro;

public static partial class Registrations
{
    [GenerateServiceRegistrations(AssignableTo = typeof(IHandler<,>), CustomHandler = nameof(AddHandler))]
    public static partial void AddHandlers();

    private static void AddHandler<THandler, TQuery, TResult>()
        where THandler : class, IHandler<TQuery, TResult>
        where TQuery   : class, IQuery<TResult>
    {
    }
}

public interface IQuery<T> { }
public interface IHandler<TQuery, TResult> where TQuery : IQuery<TResult> { }

public class BytesQuery       : IQuery<byte[]>   { }
public class JaggedBytesQuery : IQuery<byte[][]> { }
public class ScalarQuery      : IQuery<int>      { }

public class BytesHandler       : IHandler<BytesQuery,       byte[]>   { }
public class JaggedBytesHandler : IHandler<JaggedBytesQuery, byte[][]> { }
public class ScalarHandler      : IHandler<ScalarQuery,      int>      { }

Expected emit: all three AddHandler<...>() calls.
Actual emit: only AddHandler<ScalarHandler, ScalarQuery, int>(); — the two array-result handlers are silently dropped.

Root cause

MatchedTypeSatisfiesConstraints walks THandler's constraint chain recursively. On the outer step it matches THandler : IHandler<TQuery, TResult> fine. Then it recurses into TQuery : IQuery<TResult> by walking BytesQuery.AllInterfaces for IQuery<byte[]>. That step compares constraint type args with matched-interface type args:

if (matchedType.TypeArguments[i] is not INamedTypeSymbol candidateTypeArgument)
    return false;

matchedType.TypeArguments[0] = byte[] is an IArrayTypeSymbol, not INamedTypeSymbol, so the pattern-match fails and the whole outer handler is rejected.

Fix

Widen the check to ITypeSymbol and branch on shape:

  • INamedTypeSymbol candidate → recurse into SatisfiesGenericConstraints as before.
  • Other ITypeSymbol (array, pointer, dynamic) → can't recurse into nested constraints, but the simple constraints (class / struct / new() / interface / base-class) on the type parameter still apply. Reject struct, unmanaged, new(); honor class; and check interface / base-class constraint types against the candidate's AllInterfaces + BaseType.

Test

CustomHandlerTests.CustomHandler_HandlesArrayTypeArgumentInNestedConstraint covers byte[], byte[][], string[], and int. Verified: fails on main, passes with the fix; all 94 existing tests still pass.

Impact

Any consumer using CustomHandler (or ScanForTypes+Handler) with a nested-generic constraint chain and array-typed generic arguments — I hit it in production migrating a large CQRS codebase where IAsyncQueryHandler<Q, byte[]> handlers silently disappeared from DI after switching to source-gen.

…chains

MatchedTypeSatisfiesConstraints previously required every candidate type
argument to be an INamedTypeSymbol. That silently rejected any handler whose
bound generic contains an array (byte[], string[], T[][]) or other non-named
type when the callback's constraint chain recurses through a nested
IInterface<TOtherParam> where TOtherParam is bound to the array type.

Example that was dropped without diagnostic:

  interface IQuery<T> { }
  interface IHandler<Q, R> where Q : IQuery<R> { }
  class BytesQuery : IQuery<byte[]> { }
  class BytesHandler : IHandler<BytesQuery, byte[]> { }

  [GenerateServiceRegistrations(AssignableTo = typeof(IHandler<,>),
                                CustomHandler = nameof(AddHandler))]
  ...
  void AddHandler<THandler, TQuery, TResult>()
      where THandler : class, IHandler<TQuery, TResult>
      where TQuery   : class, IQuery<TResult>;

BytesHandler was skipped because the inner recursion hit IQuery<byte[]> whose
type argument byte[] is an IArrayTypeSymbol, not INamedTypeSymbol.

Widen the check to ITypeSymbol and branch on shape:
- INamedTypeSymbol: recurse into SatisfiesGenericConstraints as before.
- Other ITypeSymbol (arrays / pointers / dynamic): can't recurse, but the
  simple constraints on the type parameter still apply — reject value-type /
  unmanaged / ctor constraints, honor reference-type, and walk AllInterfaces
  + BaseType for the type constraints. Arrays are reference types
  implementing IList<T> etc., so this covers the common cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant