Skip to content

Resolve type variables through generic superclasses - #825

Open
oryan-block wants to merge 3 commits into
masterfrom
bugfix/460
Open

oryan-block wants to merge 3 commits into
masterfrom
bugfix/460

Conversation

@oryan-block

Copy link
Copy Markdown
Collaborator

Fixes #460
Fixes #218

Checklist

  • Pull requests follows the contribution guide
  • New or modified functionality is covered by tests

Description

An object type backed by a class that inherits a type variable through more than one generic superclass fails to build. Take marcust's example from #460: ConcreteItem extends BaseItem<Long>, BaseItem<T> extends AbstractItem<T>, and public T id declared on AbstractItem. It used to fail with TypeUtils.getRawType(type, declaringType) must not be null, and since #819 it's a StackOverflowError. If the superclasses rename the variable, like the Identifiable<I> / IdentifiableAdapter<K> / AuditableEntity<E> chain in #218, it fails with IllegalStateException: No type variable found for: ...IdentifiableAdapter:K. If a superclass reorders them (B<T, U> extends A<U, T>), it silently picks the wrong argument, which surfaces as a FieldResolverError on the wrong class or as a wrong mapping.

GenericType.RelativeTo resolved type variables against the declaring type, which only knows the arguments passed in by its direct subclass. When those were type variables too, unwrapGenericType(declaringType, type) fell back to TypeUtils.determineTypeArguments against the first parameterized superclass and matched the result by name (that's the 6.0.0 fix for #218). When every level uses the same name, it resolves AbstractItem.T to BaseItem.T and loops forever. With different names it finds nothing, and with reordered names it finds the wrong one.

Now replaceTypeVariable resolves every type variable declared by a class with TypeUtils.getTypeArguments(mostSpecificType, genericDeclaration), which is what getRawClass already does. It then keeps resolving whatever the variable is bound to, so nested arguments like List<Edge<T>> come out fully resolved. It also walks the owner types of mostSpecificType for variables of an outer class (Listing<T>.Entry), and it resolves wildcard bounds (List<? extends T>, which Kotlin emits for List<T> parameters). I removed the name matching (parameterizedDeclaringTypeOrSuperType and unwrapGenericType(declaringType, type)). Every caller's declaring type is the most specific type or one of its supertypes, so it can't resolve anything the new lookup can't. A variable that still can't be resolved now fails with Could not resolve type variable 'T' of X relative to Y instead of looping. A variable bound to a type containing itself can only leak out of a raw type (e.g. a raw Tree returning Tree<List<T>>). It's now erased to its bound, the way the raw type would erase it.

The original #460 case (an input type inheriting List<T>) already works on master since #819. I added a test for it with an extra superclass that renames the variable, and that version does fail on master. I didn't add T[] support. It fails with the same clear error as before, and fixing it needs GenericArrayType handling in TypeClassMatcher too. I also didn't fix resolver arguments typed T, which are still passed to Jackson unresolved at runtime, same as before. RelativeTo.declaringType is now only used in the error message. Removing it would touch the FieldResolver and SchemaClassScanner call sites, so I left that for a follow-up.

Behaviour change: generic types returned by methods inherited from a generic superclass now keep their resolved arguments (e.g. Meta<Owner>), as inherited fields already did. So if two subclasses bind them differently and both map to the same GraphQL type, the build fails with "Two different classes used for type Meta". That's the same thing that happens when Page<A> and Page<B> are referenced directly (#343). On 14.0.3 these schemas built as long as the shared type had no field depending on T. The error now prints the parameterized types (Meta<Owner> vs Meta<Tag>) instead of the same raw class twice. Also, a type variable declared by a generic method (<T> T owner()) used to be matched by name to a class variable with the same name when the resolver had a parameterized superclass. It now fails with the error above.

🤖 Generated with Claude Code

oryan-block and others added 3 commits October 3, 2026 14:18
Type variables in field and method types were resolved against the
declaring type, which only knows the arguments passed in by its direct
subclass. When those were type variables too (e.g. Item extends
BaseItem<Long> extends AbstractItem<T>), the fallback matched them by
name against the first parameterized superclass. That recursed forever
when the names repeated, failed with "No type variable found" when they
differed, and silently picked the wrong argument when a subclass
reordered them.

Resolve them against the most specific type instead, which binds the
type variables of all its supertypes, or its owner types for those of
outer classes, and drop the name matching. Variables in wildcard bounds
(e.g. List<? extends T>, which Kotlin emits for List<T> parameters) are
resolved too. A variable that can't be resolved, such as one declared
by a generic method, fails with a clear error. One bound to a type
containing itself, which can only leak out of a raw type, is erased
like the raw type would instead of being expanded endlessly.

Generic types returned by methods of a generic superclass now keep
their resolved arguments, as fields already did. So two subclasses
binding them differently can't share one GraphQL type anymore, like
direct references to Page<A> and Page<B>. The "Two different classes"
error now prints the parameterized types to show the difference.

Fixes #460
Fixes #218

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Oct 3, 2026

Copy link
Copy Markdown

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.

Building schema parser fails when java.util.List is used with a type parameter from a generic class Generic parameter not resolved

1 participant