From e2d00ec0f4d113d526c6905356e3c8c437674ef7 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 8 Oct 2026 17:40:07 -0700 Subject: [PATCH 01/11] Fix #127 --- VERSION.txt | 1 + .../com/fasterxml/classmate/TypeResolver.java | 9 +++ .../classmate/TestSubtypeResolution.java | 73 +++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/VERSION.txt b/VERSION.txt index 099f511..d7fcea6 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -21,6 +21,7 @@ Release notes: (`ResolvedRecursiveType`) in their type parameters (at any level of nesting, including array element types) are no longer cached (but see #128 for self-references via supertypes) +#127: `resolveSubtype()` does not verify or resolve array element types 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index b1055fa..51b41d3 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -619,6 +619,15 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) ((TypePlaceHolder) act).actualType(exp); return true; } + // [classmate#127]: Array types have no type parameters, so need to verify + // (and resolve) element types instead. Must be done before erased type check + // since array of placeholder has erased type of `Object[]` + if (exp.isArray() || act.isArray()) { + if (!exp.isArray() || !act.isArray()) { + return false; + } + return _verifyAndResolve(exp.getArrayElementType(), act.getArrayElementType()); + } // if not, try to verify compatibility. But note that we can not // use simple equality as we need to resolve recursively if (exp.getErasedType() != act.getErasedType()) { diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index 8a44f5c..0bdf663 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -31,6 +31,13 @@ static class Wrapper { static class ListWrapper extends Wrapper> { } + // [classmate#127] + static class ArrayWrapper extends Wrapper { } + + static class IntListArrayWrapper extends Wrapper[]> { } + + static class IntArrayWrapper extends Wrapper { } + abstract static class OuterType extends AbstractMap> { public abstract class Inner extends AbstractMap> { @@ -75,6 +82,40 @@ public void testSubtypeWithNestedPlaceholderNotCached() assertEquals(size, cache.size()); } + // [classmate#127]: type variables within array types must be resolved + public void testSubtypeWithArrayOfTypeVariable() + { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, String[].class); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, ArrayWrapper.class); + assertSame(ArrayWrapper.class, subtype.getErasedType()); + List params = subtype.getTypeParameters(); + assertEquals(1, params.size()); + assertSame(String.class, params.get(0).getErasedType()); + + // and same with generic element type + supertype = typeResolver.resolve(Wrapper.class, + typeResolver.arrayType(typeResolver.resolve(List.class, Long.class))); + subtype = typeResolver.resolveSubtype(supertype, ArrayWrapper.class); + params = subtype.getTypeParameters(); + assertEquals(1, params.size()); + assertSame(List.class, params.get(0).getErasedType()); + assertSame(Long.class, params.get(0).getTypeParameters().get(0).getErasedType()); + } + + // [classmate#127]: matching generic array element types are accepted + public void testSubtypeWithMatchingGenericArray() + { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, + typeResolver.arrayType(typeResolver.resolve(List.class, Integer.class))); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, IntListArrayWrapper.class); + assertSame(IntListArrayWrapper.class, subtype.getErasedType()); + assertEquals(supertype, subtype.getParentClass()); + + supertype = typeResolver.resolve(Wrapper.class, int[].class); + subtype = typeResolver.resolveSubtype(supertype, IntArrayWrapper.class); + assertEquals(supertype, subtype.getParentClass()); + } + /** * Test to ensure a properly parameterized {@link List} can be be made * more specific while still keeping parameterization. @@ -308,4 +349,36 @@ public void testIncompatibleTypeParametersMap() verifyException(e, "Type parameter #2/2 differs; expected java.lang.Integer"); } } + + // [classmate#127]: array element types must be verified + public void testIncompatibleGenericArrayElementType() + { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, + typeResolver.arrayType(typeResolver.resolve(List.class, String.class))); + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, IntListArrayWrapper.class); + fail("Expected failure, got: "+t); + } catch (IllegalArgumentException e) { + verifyException(e, "Type parameter #1/1 differs"); + } + } + + public void testIncompatibleArrayElementType() + { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, long[].class); + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, IntArrayWrapper.class); + fail("Expected failure, got: "+t); + } catch (IllegalArgumentException e) { + verifyException(e, "Type parameter #1/1 differs"); + } + + supertype = typeResolver.resolve(Wrapper.class, String.class); + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, ArrayWrapper.class); + fail("Expected failure, got: "+t); + } catch (IllegalArgumentException e) { + verifyException(e, "Type parameter #1/1 differs"); + } + } } From b70475546ce8526bc68354e1148016565009db8e Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 8 Oct 2026 17:42:04 -0700 Subject: [PATCH 02/11] ... --- VERSION.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/VERSION.txt b/VERSION.txt index d7fcea6..f6cdc46 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -22,6 +22,7 @@ Release notes: including array element types) are no longer cached (but see #128 for self-references via supertypes) #127: `resolveSubtype()` does not verify or resolve array element types + (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) From 700f075d4d4609702dfbe60a2003fb34df7c4d59 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 8 Oct 2026 17:47:53 -0700 Subject: [PATCH 03/11] Fix the crappy initial impl by Claude --- .../com/fasterxml/classmate/TypeResolver.java | 37 ++++++--- .../classmate/TestSubtypeResolution.java | 78 +++++++++++++++++++ 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 51b41d3..bbd5fd2 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -614,18 +614,33 @@ private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actu private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) { + // [classmate#127]: Self-reference in expected type is only valid within its + // original resolution context, so need to re-resolve it as a stand-alone type + if (exp instanceof ResolvedRecursiveType) { + exp = _fromClass(null, exp.getErasedType(), exp.getTypeBindings()); + } // See if we have an actual type placeholder to resolve; if yes, replace if (act instanceof TypePlaceHolder) { - ((TypePlaceHolder) act).actualType(exp); + // [classmate#127]: primitive types are not valid type parameters + if (exp.isPrimitive()) { + return false; + } + TypePlaceHolder placeholder = (TypePlaceHolder) act; + // [classmate#127]: and if already bound, must be bound to the same type + ResolvedType prev = placeholder.actualType(); + if (prev != null) { + return prev.equals(exp); + } + placeholder.actualType(exp); return true; } // [classmate#127]: Array types have no type parameters, so need to verify // (and resolve) element types instead. Must be done before erased type check // since array of placeholder has erased type of `Object[]` - if (exp.isArray() || act.isArray()) { - if (!exp.isArray() || !act.isArray()) { - return false; - } + if (exp.isArray() != act.isArray()) { + return false; + } + if (exp.isArray()) { return _verifyAndResolve(exp.getArrayElementType(), act.getArrayElementType()); } // if not, try to verify compatibility. But note that we can not @@ -636,10 +651,14 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) // But we can check type parameters "blindly" List expectedTypes = exp.getTypeParameters(); List actualTypes = act.getTypeParameters(); - for (int i = 0, len = expectedTypes.size(); i < len; ++i) { - ResolvedType exp2 = expectedTypes.get(i); - ResolvedType act2 = actualTypes.get(i); - if (!_verifyAndResolve(exp2, act2)) { + final int len = expectedTypes.size(); + if (len != actualTypes.size()) { + // [classmate#127]: raw self-reference (like `Base` within `class Base extends Wrapper`) + // has no type parameters; can not verify those, but there are no placeholders to resolve either + return (act instanceof ResolvedRecursiveType) && actualTypes.isEmpty(); + } + for (int i = 0; i < len; ++i) { + if (!_verifyAndResolve(expectedTypes.get(i), actualTypes.get(i))) { return false; } } diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index 0bdf663..36c9efa 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -38,6 +38,22 @@ static class IntListArrayWrapper extends Wrapper[]> { } static class IntArrayWrapper extends Wrapper { } + static class Array2Wrapper extends Wrapper { } + + static class ListOfArrayWrapper extends Wrapper> { } + + static class Pair { } + + static class DupPair extends Pair { } + + static class RawSelfArray extends Wrapper { } + + static class RawSelfArraySub extends RawSelfArray { } + + static class RawSelf extends Wrapper { } + + static class RawSelfSub extends RawSelf { } + abstract static class OuterType extends AbstractMap> { public abstract class Inner extends AbstractMap> { @@ -116,6 +132,42 @@ public void testSubtypeWithMatchingGenericArray() assertEquals(supertype, subtype.getParentClass()); } + // [classmate#127]: same type variable bound consistently is fine + public void testSubtypeWithRepeatedTypeVariable() + { + ResolvedType supertype = typeResolver.resolve(Pair.class, String[].class, String.class); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, DupPair.class); + assertSame(DupPair.class, subtype.getErasedType()); + assertSame(String.class, subtype.getTypeParameters().get(0).getErasedType()); + assertEquals(supertype, subtype.getParentClass()); + } + + // [classmate#127]: raw self-reference as (array element) type parameter + public void testSubtypeWithRawSelfReference() + { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, RawSelfArray[].class); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, RawSelfArraySub.class); + assertSame(RawSelfArraySub.class, subtype.getErasedType()); + + supertype = typeResolver.resolve(Wrapper.class, RawSelf.class); + subtype = typeResolver.resolveSubtype(supertype, RawSelfSub.class); + assertSame(RawSelfSub.class, subtype.getErasedType()); + } + + // [classmate#127]: self-reference in supertype must not leak into subtype + public void testSubtypeWithSelfReferenceInSupertype() + { + ResolvedType supertype = typeResolver.resolve(RawSelfArray.class).getParentClass(); + assertTrue(supertype.getTypeParameters().get(0).getArrayElementType() + instanceof com.fasterxml.classmate.types.ResolvedRecursiveType); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, ArrayWrapper.class); + ResolvedType param = subtype.getTypeParameters().get(0); + assertFalse(param instanceof com.fasterxml.classmate.types.ResolvedRecursiveType); + assertSame(RawSelfArray.class, param.getErasedType()); + assertNotNull(param.getParentClass()); + assertSame(Wrapper.class, param.getParentClass().getErasedType()); + } + /** * Test to ensure a properly parameterized {@link List} can be be made * more specific while still keeping parameterization. @@ -381,4 +433,30 @@ public void testIncompatibleArrayElementType() verifyException(e, "Type parameter #1/1 differs"); } } + + // [classmate#127]: primitive types can not be bound to type variables + public void testPrimitiveArrayElementForTypeVariable() + { + _verifyIncompatible(typeResolver.resolve(Wrapper.class, int[].class), ArrayWrapper.class); + _verifyIncompatible(typeResolver.resolve(Wrapper.class, int[][].class), Array2Wrapper.class); + _verifyIncompatible(typeResolver.resolve(Wrapper.class, + typeResolver.resolve(List.class, int[].class)), ListOfArrayWrapper.class); + } + + // [classmate#127]: type variable must not be bound to conflicting types + public void testConflictingTypeVariableBindings() + { + _verifyIncompatible(typeResolver.resolve(Pair.class, String[].class, Integer.class), + DupPair.class); + } + + private void _verifyIncompatible(ResolvedType supertype, Class subtype) + { + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, subtype); + fail("Expected failure, got: "+t.getFullDescription()); + } catch (IllegalArgumentException e) { + verifyException(e, "differs"); + } + } } From 534e9de138da26e26138e6080fc7a6ed1c9d0278 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 8 Oct 2026 18:11:40 -0700 Subject: [PATCH 04/11] More more more fixes --- VERSION.txt | 7 ++ .../com/fasterxml/classmate/TypeResolver.java | 74 ++++++++++++++++--- .../classmate/TestSubtypeResolution.java | 67 +++++++++++++++++ .../fasterxml/classmate/TypeResolverTest.java | 26 +++++++ 4 files changed, 164 insertions(+), 10 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index f6cdc46..65e75a5 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -22,6 +22,13 @@ Release notes: including array element types) are no longer cached (but see #128 for self-references via supertypes) #127: `resolveSubtype()` does not verify or resolve array element types +- `resolveSubtype()` now rejects type variable bound to conflicting types + (like `E` in `Dup extends Pair`) and primitive types bound to + type variables (like `int` for `E` in `E[]`) +- Self-references (`ResolvedRecursiveType`) are no longer included in subtypes + resolved by `resolveSubtype()`; raw self-references resolve to their bounds +- `TypeResolver.resolve(Type, Type...)` now fails with `IllegalArgumentException` + if any of type parameters is a primitive type (like `resolve(List.class, int.class)`) (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index bbd5fd2..17ef620 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -121,6 +121,9 @@ public TypeResolver(ResolvedTypeCache typeCache) { * Note that you can mix different types of type parameters, whether already * resolved ({@link ResolvedType}), type-erased ({@link java.lang.Class}) or * generic type reference ({@link GenericType}). + * + * @throws IllegalArgumentException If any of type parameters is a primitive type + * (since 1.8) */ public ResolvedType resolve(Type type, Type... typeParameters) { @@ -163,6 +166,12 @@ public ResolvedType resolve(Type type, Type... typeParameters) ResolvedType[] resolvedParams = new ResolvedType[len]; for (int i = 0; i < len; ++i) { resolvedParams[i] = _fromAny(null, typeParameters[i], bindings); + // [classmate#127]: primitive types are not valid type parameters + if (resolvedParams[i].isPrimitive()) { + throw new IllegalArgumentException("Can not use primitive type (" + +resolvedParams[i].getBriefDescription()+") as type parameter #" + +(i+1)+"/"+len+" of "+rawBase.getName()); + } } return _fromClass(null, rawBase, TypeBindings.create(rawBase, resolvedParams)); } @@ -235,10 +244,10 @@ public ResolvedType resolve(TypeBindings typeBindings, Type jdkType) public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtype) throws IllegalArgumentException, UnsupportedOperationException { - // first: if it's a recursive reference, find out referred-to type - ResolvedType refType = supertype.getSelfReferencedType(); - if (refType != null) { - supertype = refType; + // first: if it's a recursive reference, resolve as stand-alone type + // ([classmate#127]: not as referred-to type, which may have different bindings) + if (supertype instanceof ResolvedRecursiveType) { + supertype = _resolveSelfReferences(supertype); } // Then, trivial check for case where subtype is supertype... final Class superclass = supertype.getErasedType(); @@ -614,10 +623,14 @@ private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actu private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) { - // [classmate#127]: Self-reference in expected type is only valid within its - // original resolution context, so need to re-resolve it as a stand-alone type + // [classmate#127]: Self-references are only valid within their original + // resolution context, so need to re-resolve as stand-alone types (which also + // resolves type parameters of raw self-references to their bounds) if (exp instanceof ResolvedRecursiveType) { - exp = _fromClass(null, exp.getErasedType(), exp.getTypeBindings()); + exp = _resolveSelfReferences(exp); + } + if (act instanceof ResolvedRecursiveType) { + act = _resolveSelfReferences(act); } // See if we have an actual type placeholder to resolve; if yes, replace if (act instanceof TypePlaceHolder) { @@ -625,6 +638,8 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) if (exp.isPrimitive()) { return false; } + // [classmate#127]: self-references nested within type to bind must not leak either + exp = _resolveSelfReferences(exp); TypePlaceHolder placeholder = (TypePlaceHolder) act; // [classmate#127]: and if already bound, must be bound to the same type ResolvedType prev = placeholder.actualType(); @@ -653,9 +668,7 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) List actualTypes = act.getTypeParameters(); final int len = expectedTypes.size(); if (len != actualTypes.size()) { - // [classmate#127]: raw self-reference (like `Base` within `class Base extends Wrapper`) - // has no type parameters; can not verify those, but there are no placeholders to resolve either - return (act instanceof ResolvedRecursiveType) && actualTypes.isEmpty(); + return false; } for (int i = 0; i < len; ++i) { if (!_verifyAndResolve(expectedTypes.get(i), actualTypes.get(i))) { @@ -664,4 +677,45 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) } return true; } + + /** + * Helper method for replacing all self-references ({@link ResolvedRecursiveType}s) + * within given type (including type itself, type parameters and array element types, + * at any level of nesting) with stand-alone resolved types. + * + * @return Type with self-references replaced; given type itself if it contains none + * + * @since 1.8 + */ + private ResolvedType _resolveSelfReferences(ResolvedType type) + { + if (type.isArray()) { + ResolvedType elem = type.getArrayElementType(); + ResolvedType newElem = (elem == null) ? null : _resolveSelfReferences(elem); + if (newElem == elem) { + return type; + } + return _arrayOf(_arrayClassFor(newElem), newElem); + } + final TypeBindings bindings = type.getTypeBindings(); + ResolvedType[] newTypes = null; + for (int i = 0, len = bindings.size(); i < len; ++i) { + ResolvedType t = bindings.getBoundType(i); + ResolvedType newT = _resolveSelfReferences(t); + if (newT != t) { + if (newTypes == null) { + newTypes = bindings.getTypeParameters().toArray(new ResolvedType[0]); + } + newTypes[i] = newT; + } + } + if (newTypes != null) { + return _fromClass(null, type.getErasedType(), + TypeBindings.create(type.getErasedType(), newTypes)); + } + if (type instanceof ResolvedRecursiveType) { + return _fromClass(null, type.getErasedType(), bindings); + } + return type; + } } diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index 36c9efa..874e55e 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -54,6 +54,18 @@ static class RawSelf extends Wrapper { } static class RawSelfSub extends RawSelf { } + static class RawSelfOther extends Wrapper { } + + static class SubWrapper extends Wrapper { } + + static class SelfInList extends Wrapper> { } + + static class SamePair extends Pair { } + + static class SelfParam extends Wrapper> { } + + static class SelfParamSub extends SelfParam { } + abstract static class OuterType extends AbstractMap> { public abstract class Inner extends AbstractMap> { @@ -168,6 +180,52 @@ public void testSubtypeWithSelfReferenceInSupertype() assertSame(Wrapper.class, param.getParentClass().getErasedType()); } + // [classmate#127]: self-references nested within bound type must not leak into subtype + public void testSubtypeWithNestedSelfReferenceInSupertype() + { + ResolvedType supertype = typeResolver.resolve(RawSelfArray.class).getParentClass(); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, SubWrapper.class); + ResolvedType param = subtype.getTypeParameters().get(0); + assertTrue(param.isArray()); + _verifyNoSelfReference(param.getArrayElementType(), RawSelfArray.class); + + supertype = typeResolver.resolve(SelfInList.class).getParentClass(); + subtype = typeResolver.resolveSubtype(supertype, SubWrapper.class); + param = subtype.getTypeParameters().get(0); + assertSame(List.class, param.getErasedType()); + _verifyNoSelfReference(param.getTypeParameters().get(0), SelfInList.class); + } + + // [classmate#127]: same type with and without self-reference must bind consistently + public void testSubtypeWithSelfReferenceAndRepeatedTypeVariable() + { + ResolvedType withSelfRef = typeResolver.resolve(SelfInList.class).getParentClass() + .getTypeParameters().get(0); + ResolvedType withoutSelfRef = typeResolver.resolve(List.class, SelfInList.class); + ResolvedType supertype = typeResolver.resolve(Pair.class, withSelfRef, withoutSelfRef); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, SamePair.class); + assertEquals(withoutSelfRef, subtype.getTypeParameters().get(0)); + } + + // [classmate#127]: self-reference as supertype is resolved using its own bindings + public void testSubtypeOfSelfReference() + { + ResolvedType supertype = typeResolver.resolve(SelfParam.class, Integer.class) + .getParentClass().getTypeParameters().get(0); + assertTrue(supertype instanceof com.fasterxml.classmate.types.ResolvedRecursiveType); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, SelfParamSub.class); + assertSame(SelfParamSub.class, subtype.getErasedType()); + assertSame(String.class, subtype.getTypeParameters().get(0).getErasedType()); + } + + private void _verifyNoSelfReference(ResolvedType type, Class expRaw) + { + assertFalse(type instanceof com.fasterxml.classmate.types.ResolvedRecursiveType); + assertSame(expRaw, type.getErasedType()); + assertNotNull(type.getParentClass()); + assertSame(Wrapper.class, type.getParentClass().getErasedType()); + } + /** * Test to ensure a properly parameterized {@link List} can be be made * more specific while still keeping parameterization. @@ -459,4 +517,13 @@ private void _verifyIncompatible(ResolvedType supertype, Class subtype) verifyException(e, "differs"); } } + + // [classmate#127]: raw self-reference resolves to bounds, same as other raw types + public void testRawSelfReferenceVerifiedAsRaw() + { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, + typeResolver.resolve(RawSelf.class, String.class)); + _verifyIncompatible(supertype, RawSelfSub.class); + _verifyIncompatible(supertype, RawSelfOther.class); + } } diff --git a/src/test/java/com/fasterxml/classmate/TypeResolverTest.java b/src/test/java/com/fasterxml/classmate/TypeResolverTest.java index bf34231..a5464d1 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolverTest.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolverTest.java @@ -354,6 +354,32 @@ public void testGenericParamMismatch() } } + // [classmate#127]: primitive types are not valid type parameters + public void testPrimitiveTypeParameter() + { + try { + typeResolver.resolve(List.class, int.class); + fail("Expected failure"); + } catch (IllegalArgumentException e) { + verifyException(e, "Can not use primitive type (int) as type parameter #1/1 of java.util.List"); + } + try { + typeResolver.resolve(Map.class, String.class, typeResolver.resolve(Long.TYPE)); + fail("Expected failure"); + } catch (IllegalArgumentException e) { + verifyException(e, "Can not use primitive type (long) as type parameter #2/2 of java.util.Map"); + } + try { + typeResolver.resolve(new GenericType>() { }, Void.TYPE); + fail("Expected failure"); + } catch (IllegalArgumentException e) { + verifyException(e, "Can not use primitive type (void) as type parameter #1/1"); + } + // but arrays of primitives are fine + ResolvedType t = typeResolver.resolve(List.class, int[].class); + assertSame(int[].class, t.getTypeParameters().get(0).getErasedType()); + } + public void testInvalidSubtype() { ResolvedType supertype = typeResolver.resolve(String.class); From 6da8ff4eadff485d3acbaf17c8a7a071da1da0ad Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 8 Oct 2026 18:26:33 -0700 Subject: [PATCH 05/11] Claude trying to untangle out of the mess... --- VERSION.txt | 12 +- .../com/fasterxml/classmate/TypeResolver.java | 131 ++++++++++++------ .../classmate/types/TypePlaceHolder.java | 7 + .../classmate/TestSubtypeResolution.java | 63 ++++++++- .../fasterxml/classmate/TypeResolverTest.java | 10 +- 5 files changed, 166 insertions(+), 57 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 65e75a5..f27a634 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -23,10 +23,14 @@ Release notes: self-references via supertypes) #127: `resolveSubtype()` does not verify or resolve array element types - `resolveSubtype()` now rejects type variable bound to conflicting types - (like `E` in `Dup extends Pair`) and primitive types bound to - type variables (like `int` for `E` in `E[]`) -- Self-references (`ResolvedRecursiveType`) are no longer included in subtypes - resolved by `resolveSubtype()`; raw self-references resolve to their bounds + (like `E` in `Dup extends Pair`; except that `Object`, which may + come from wildcard or raw type, is compatible with more specific type) and + primitive types bound to type variables (like `int` for `E` in `E[]`) +- Self-references (`ResolvedRecursiveType`) only valid within their original + resolution context (like ones in type parameters of `getParentClass()`) are no + longer included in types returned by `resolveSubtype()`; raw self-references + resolve to their bounds (self-references to enclosing type, like in raw `Enum`, + are retained as-is) - `TypeResolver.resolve(Type, Type...)` now fails with `IllegalArgumentException` if any of type parameters is a primitive type (like `resolve(List.class, int.class)`) (fix by @cowtowncoder w/ Claude code) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 17ef620..e3f0a2e 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -244,11 +244,10 @@ public ResolvedType resolve(TypeBindings typeBindings, Type jdkType) public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtype) throws IllegalArgumentException, UnsupportedOperationException { - // first: if it's a recursive reference, resolve as stand-alone type - // ([classmate#127]: not as referred-to type, which may have different bindings) - if (supertype instanceof ResolvedRecursiveType) { - supertype = _resolveSelfReferences(supertype); - } + // first: [classmate#127] replace self-references only valid within their original + // resolution context (like supertype itself being one, or one nested within supertype + // obtained from `getParentClass()`) with stand-alone types + supertype = _resolveSelfReferences(supertype, null); // Then, trivial check for case where subtype is supertype... final Class superclass = supertype.getErasedType(); if (superclass == subtype) { // unlikely but cheap check so let's just do it @@ -290,7 +289,7 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp } // Ok, then, let's find and verify type assignments; resolve type holders if any // (and yes, even for no-type-parameters case) - _resolveTypePlaceholders(supertype, resolvedSupertype); + _resolveTypePlaceholders(supertype, resolvedSupertype, subtype); // And then re-construct, if necessary if (paramCount == 0) { // if no type parameters, fine as is return resolvedSubtype; @@ -606,7 +605,8 @@ private ResolvedType _fromVariable(ClassStack context, TypeVariable variable, * @param sourceType Original base type used for specification/refinement * @param actualType Base type instance after re-resolving, possibly containing type placeholders */ - private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actualType) + private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actualType, + Class subtype) throws IllegalArgumentException { List expectedTypes = sourceType.getTypeParameters(); @@ -614,40 +614,54 @@ private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actu for (int i = 0, len = expectedTypes.size(); i < len; ++i) { ResolvedType exp = expectedTypes.get(i); ResolvedType act = actualTypes.get(i); - if (!_verifyAndResolve(exp, act)) { + if (!_verifyAndResolve(exp, act, subtype)) { throw new IllegalArgumentException("Type parameter #"+(i+1)+"/"+len+" differs; expected " +exp.getBriefDescription()+", got "+act.getBriefDescription()); } } } - private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) + /** + * @param exp Expected type, from supertype being refined (with all self-references + * not valid outside their resolution context already replaced) + * @param act Actual type, from re-resolved subtype; may contain placeholders + */ + private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act, Class subtype) { - // [classmate#127]: Self-references are only valid within their original - // resolution context, so need to re-resolve as stand-alone types (which also - // resolves type parameters of raw self-references to their bounds) - if (exp instanceof ResolvedRecursiveType) { - exp = _resolveSelfReferences(exp); - } - if (act instanceof ResolvedRecursiveType) { - act = _resolveSelfReferences(act); - } // See if we have an actual type placeholder to resolve; if yes, replace if (act instanceof TypePlaceHolder) { // [classmate#127]: primitive types are not valid type parameters if (exp.isPrimitive()) { return false; } - // [classmate#127]: self-references nested within type to bind must not leak either - exp = _resolveSelfReferences(exp); TypePlaceHolder placeholder = (TypePlaceHolder) act; - // [classmate#127]: and if already bound, must be bound to the same type ResolvedType prev = placeholder.actualType(); - if (prev != null) { - return prev.equals(exp); + if (prev == null) { + placeholder.actualType(exp); + return true; + } + // [classmate#127]: if already bound, must be bound to the same type; except + // that `Object` (which may come from wildcard or raw type) is compatible with + // anything, in which case the more specific type is retained + if (prev.equals(exp) || _isJavaLangObject(exp)) { + return true; } - placeholder.actualType(exp); - return true; + if (_isJavaLangObject(prev)) { + placeholder.actualType(exp); + return true; + } + throw new IllegalArgumentException(String.format( +"Conflicting bindings for type variable `%s` of %s: %s vs %s", + subtype.getTypeParameters()[placeholder.ordinal()].getName(), subtype.getName(), + prev.getBriefDescription(), exp.getBriefDescription())); + } + // [classmate#127]: raw self-reference needs to be resolved to its bounds to be + // comparable with other types (but not with another self-reference) + if (!(exp instanceof ResolvedRecursiveType)) { + act = _resolveRawSelfReference(act); + } + if (!(act instanceof ResolvedRecursiveType)) { + exp = _resolveRawSelfReference(exp); } // [classmate#127]: Array types have no type parameters, so need to verify // (and resolve) element types instead. Must be done before erased type check @@ -656,7 +670,7 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) return false; } if (exp.isArray()) { - return _verifyAndResolve(exp.getArrayElementType(), act.getArrayElementType()); + return _verifyAndResolve(exp.getArrayElementType(), act.getArrayElementType(), subtype); } // if not, try to verify compatibility. But note that we can not // use simple equality as we need to resolve recursively @@ -671,37 +685,73 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) return false; } for (int i = 0; i < len; ++i) { - if (!_verifyAndResolve(expectedTypes.get(i), actualTypes.get(i))) { + if (!_verifyAndResolve(expectedTypes.get(i), actualTypes.get(i), subtype)) { return false; } } return true; } + private static boolean _isJavaLangObject(ResolvedType type) { + return type.getErasedType() == Object.class; + } + /** - * Helper method for replacing all self-references ({@link ResolvedRecursiveType}s) - * within given type (including type itself, type parameters and array element types, - * at any level of nesting) with stand-alone resolved types. + * Helper method for resolving raw self-reference (one without type bindings, for + * generic class) into type with type parameters bound to their bounds; same as + * other raw types are resolved. + * + * @since 1.8 + */ + private ResolvedType _resolveRawSelfReference(ResolvedType type) + { + if ((type instanceof ResolvedRecursiveType) + && type.getTypeBindings().isEmpty() + && type.getErasedType().getTypeParameters().length > 0) { + return _fromClass(null, type.getErasedType(), type.getTypeBindings()); + } + return type; + } + + /** + * Helper method for replacing self-references ({@link ResolvedRecursiveType}s) + * that are not valid outside their original resolution context with stand-alone + * resolved types. This includes the type itself, as well as ones within type + * parameters and array element types, at any level of nesting: except for + * self-references to an enclosing type (like {@code E} in {@code Enum>}) + * which are valid as-is. + * + * @param enclosing Erased types of enclosing types (containing given type + * as type parameter); {@code null} if none * * @return Type with self-references replaced; given type itself if it contains none * * @since 1.8 */ - private ResolvedType _resolveSelfReferences(ResolvedType type) + private ResolvedType _resolveSelfReferences(ResolvedType type, ClassStack enclosing) { + if (!TypeBindings.isContextual(type)) { + return type; + } if (type.isArray()) { ResolvedType elem = type.getArrayElementType(); - ResolvedType newElem = (elem == null) ? null : _resolveSelfReferences(elem); - if (newElem == elem) { - return type; - } - return _arrayOf(_arrayClassFor(newElem), newElem); + ResolvedType newElem = _resolveSelfReferences(elem, enclosing); + return (newElem == elem) ? type : _arrayOf(type.getErasedType(), newElem); + } + final Class raw = type.getErasedType(); + final boolean selfRef = (type instanceof ResolvedRecursiveType); + if (selfRef && (enclosing != null) && (enclosing.find(raw) != null)) { + return type; } + // Self-reference itself is not an enclosing type for its bindings + // (since it is replaced); otherwise it is + ClassStack nextEnclosing = selfRef ? enclosing + : ((enclosing == null) ? new ClassStack(raw) : enclosing.child(raw)); final TypeBindings bindings = type.getTypeBindings(); ResolvedType[] newTypes = null; for (int i = 0, len = bindings.size(); i < len; ++i) { ResolvedType t = bindings.getBoundType(i); - ResolvedType newT = _resolveSelfReferences(t); + ResolvedType newT = _resolveSelfReferences(t, nextEnclosing); if (newT != t) { if (newTypes == null) { newTypes = bindings.getTypeParameters().toArray(new ResolvedType[0]); @@ -710,11 +760,10 @@ private ResolvedType _resolveSelfReferences(ResolvedType type) } } if (newTypes != null) { - return _fromClass(null, type.getErasedType(), - TypeBindings.create(type.getErasedType(), newTypes)); + return _fromClass(null, raw, TypeBindings.create(raw, newTypes)); } - if (type instanceof ResolvedRecursiveType) { - return _fromClass(null, type.getErasedType(), bindings); + if (selfRef) { + return _fromClass(null, raw, bindings); } return type; } diff --git a/src/main/java/com/fasterxml/classmate/types/TypePlaceHolder.java b/src/main/java/com/fasterxml/classmate/types/TypePlaceHolder.java index a441e96..ec3b286 100644 --- a/src/main/java/com/fasterxml/classmate/types/TypePlaceHolder.java +++ b/src/main/java/com/fasterxml/classmate/types/TypePlaceHolder.java @@ -28,6 +28,13 @@ public TypePlaceHolder(int ordinal) @Override public boolean canCreateSubtypes() { return false; } + /** + * @return Index of type parameter (of the type being resolved) this placeholder is for + * + * @since 1.8 + */ + public int ordinal() { return _ordinal; } + public ResolvedType actualType() { return _actualType; } public void actualType(ResolvedType t) { _actualType = t; } diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index 874e55e..fec70f4 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -66,6 +66,12 @@ static class SelfParam extends Wrapper> { } static class SelfParamSub extends SelfParam { } + static class Builder> { } + + static class NPair { } + + static class NSamePair extends NPair { } + abstract static class OuterType extends AbstractMap> { public abstract class Inner extends AbstractMap> { @@ -170,11 +176,10 @@ public void testSubtypeWithRawSelfReference() public void testSubtypeWithSelfReferenceInSupertype() { ResolvedType supertype = typeResolver.resolve(RawSelfArray.class).getParentClass(); - assertTrue(supertype.getTypeParameters().get(0).getArrayElementType() - instanceof com.fasterxml.classmate.types.ResolvedRecursiveType); + assertTrue(TypeResolver.isSelfReference(supertype.getTypeParameters().get(0).getArrayElementType())); ResolvedType subtype = typeResolver.resolveSubtype(supertype, ArrayWrapper.class); ResolvedType param = subtype.getTypeParameters().get(0); - assertFalse(param instanceof com.fasterxml.classmate.types.ResolvedRecursiveType); + assertFalse(TypeResolver.isSelfReference(param)); assertSame(RawSelfArray.class, param.getErasedType()); assertNotNull(param.getParentClass()); assertSame(Wrapper.class, param.getParentClass().getErasedType()); @@ -212,15 +217,53 @@ public void testSubtypeOfSelfReference() { ResolvedType supertype = typeResolver.resolve(SelfParam.class, Integer.class) .getParentClass().getTypeParameters().get(0); - assertTrue(supertype instanceof com.fasterxml.classmate.types.ResolvedRecursiveType); + assertTrue(TypeResolver.isSelfReference(supertype)); ResolvedType subtype = typeResolver.resolveSubtype(supertype, SelfParamSub.class); assertSame(SelfParamSub.class, subtype.getErasedType()); assertSame(String.class, subtype.getTypeParameters().get(0).getErasedType()); } + // [classmate#127]: self-references valid as-is (like in raw `Enum`) must be retained + public void testSubtypeWithSelfBoundedTypeParameter() + { + for (Class selfBounded : new Class[] { Enum.class, Builder.class }) { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, selfBounded); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, SubWrapper.class); + assertEquals(typeResolver.resolve(SubWrapper.class, selfBounded), subtype); + assertEquals(supertype, subtype.getParentClass()); + } + } + + // [classmate#127]: `Object` (from wildcard or raw type) compatible with more specific binding + public void testSubtypeWithWildcardAndRepeatedTypeVariable() + { + ResolvedType supertype = typeResolver.resolve(new GenericType>() { }); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, java.util.function.UnaryOperator.class); + assertEquals(typeResolver.resolve(java.util.function.UnaryOperator.class, String.class), subtype); + + supertype = typeResolver.resolve(new GenericType>() { }); + subtype = typeResolver.resolveSubtype(supertype, java.util.function.UnaryOperator.class); + assertEquals(typeResolver.resolve(java.util.function.UnaryOperator.class, String.class), subtype); + + // raw `NPair` resolves to `NPair` + supertype = typeResolver.resolve(NPair.class); + subtype = typeResolver.resolveSubtype(supertype, NSamePair.class); + assertEquals(typeResolver.resolve(NSamePair.class, Number.class), subtype); + } + + // [classmate#127]: no self-references in result even if no actual sub-classing done + public void testSubtypeSameAsSupertypeWithSelfReference() + { + ResolvedType supertype = typeResolver.resolve(RawSelfArray.class).getParentClass(); + ResolvedType result = typeResolver.resolveSubtype(supertype, Wrapper.class); + assertSame(Wrapper.class, result.getErasedType()); + _verifyNoSelfReference(result.getTypeParameters().get(0).getArrayElementType(), + RawSelfArray.class); + } + private void _verifyNoSelfReference(ResolvedType type, Class expRaw) { - assertFalse(type instanceof com.fasterxml.classmate.types.ResolvedRecursiveType); + assertFalse(TypeResolver.isSelfReference(type)); assertSame(expRaw, type.getErasedType()); assertNotNull(type.getParentClass()); assertSame(Wrapper.class, type.getParentClass().getErasedType()); @@ -504,8 +547,14 @@ public void testPrimitiveArrayElementForTypeVariable() // [classmate#127]: type variable must not be bound to conflicting types public void testConflictingTypeVariableBindings() { - _verifyIncompatible(typeResolver.resolve(Pair.class, String[].class, Integer.class), - DupPair.class); + ResolvedType supertype = typeResolver.resolve(Pair.class, String[].class, Integer.class); + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, DupPair.class); + fail("Expected failure, got: "+t.getFullDescription()); + } catch (IllegalArgumentException e) { + verifyException(e, "Conflicting bindings for type variable `E` of "+DupPair.class.getName() + +": java.lang.String vs java.lang.Integer"); + } } private void _verifyIncompatible(ResolvedType supertype, Class subtype) diff --git a/src/test/java/com/fasterxml/classmate/TypeResolverTest.java b/src/test/java/com/fasterxml/classmate/TypeResolverTest.java index a5464d1..f9b7b13 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolverTest.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolverTest.java @@ -508,21 +508,21 @@ public void testTypesMatch() throws IllegalAccessException, InvocationTargetExce // first test equality GenericType>> matchBList = new GenericType>>() { }; ResolvedType matchBListResolved = typeResolver.resolve(matchBList); - assertTrue((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchBListResolved)); + assertTrue((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchBListResolved, Object.class)); GenericType, Comparator>>> matchASet = new GenericType, Comparator>>>() { }; GenericType, Comparator>>> matchASet1 = new GenericType, Comparator>>>() { }; ResolvedType matchASetResolved = typeResolver.resolve(matchASet); ResolvedType matchASetResolved1 = typeResolver.resolve(matchASet1); - assertTrue((Boolean) typesMatchMethod.invoke(typeResolver, matchASetResolved, matchASetResolved1)); + assertTrue((Boolean) typesMatchMethod.invoke(typeResolver, matchASetResolved, matchASetResolved1, Object.class)); // now check inequality GenericType, Comparator>>> matchAList = new GenericType, Comparator>>>() { }; ResolvedType matchAListResolved = typeResolver.resolve(matchAList); - assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchASetResolved)); + assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchASetResolved, Object.class)); // now ensure different number of type-parameters are handled correctly - assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchBListResolved)); - assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchAListResolved)); + assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchBListResolved, Object.class)); + assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchAListResolved, Object.class)); } public void testMultiDimensionalGenericArrays() throws Exception From aab81e6f96a574514d759eb826dcc9c883d4bf8f Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 8 Oct 2026 18:35:09 -0700 Subject: [PATCH 06/11] Yet More Fixes --- VERSION.txt | 6 ++ .../com/fasterxml/classmate/TypeResolver.java | 88 +++++++++++++++++-- .../classmate/TestSubtypeResolution.java | 73 +++++++++++++++ 3 files changed, 160 insertions(+), 7 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index f27a634..51bc3d9 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -31,6 +31,12 @@ Release notes: longer included in types returned by `resolveSubtype()`; raw self-references resolve to their bounds (self-references to enclosing type, like in raw `Enum`, are retained as-is) +- `resolveSubtype()` now accepts `Object` (from wildcard or raw type, like raw + `Wrapper` or `Wrapper`) as compatible with any type parameter of subtype + (like `E[]` of `ArrayWrapper extends Wrapper`); type variables bound only + this way resolve to their bounds (`ArrayWrapper`) +- `resolveSubtype()` now verifies that type parameters of subtype satisfy (type-erased) + bounds of its type variables (f.ex rejects `NumArray` with `E` of `String`) - `TypeResolver.resolve(Type, Type...)` now fails with `IllegalArgumentException` if any of type parameters is a primitive type (like `resolve(List.class, int.class)`) (fix by @cowtowncoder w/ Claude code) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index e3f0a2e..1a830f7 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -247,7 +247,9 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp // first: [classmate#127] replace self-references only valid within their original // resolution context (like supertype itself being one, or one nested within supertype // obtained from `getParentClass()`) with stand-alone types - supertype = _resolveSelfReferences(supertype, null); + if (TypeBindings.isContextual(supertype)) { + supertype = _resolveSelfReferences(supertype, new ArrayList<>()); + } // Then, trivial check for case where subtype is supertype... final Class superclass = supertype.getErasedType(); if (superclass == subtype) { // unlikely but cheap check so let's just do it @@ -296,6 +298,9 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp } // but with type parameters, need to reconstruct final ResolvedType[] typeParams = new ResolvedType[paramCount]; + // [classmate#127]: `Object` (from wildcard or raw type) to be replaced by bound + // of type variable; same as for raw subtype + List rawParams = null; for (int i = 0; i < paramCount; ++i) { ResolvedType t = placeholders[i].actualType(); // Is it ok for it to be left unassigned? For now let's not allow that @@ -306,11 +311,49 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp throw new IllegalArgumentException("Failed to find type parameter #"+(i+1)+"/" +paramCount+" for "+subtype.getName()); } + if (_isJavaLangObject(t)) { + if (rawParams == null) { + rawParams = _fromClass(null, subtype, TypeBindings.emptyBindings()).getTypeParameters(); + } + t = rawParams.get(i); + } typeParams[i] = t; } + _verifyBounds(subtype, typeParams); return resolve(subtype, typeParams); } + /** + * Helper method for verifying that given type parameters satisfy declared bounds + * of type variables of given type (as far as type-erased types are concerned). + * + * @since 1.8 + */ + private void _verifyBounds(Class rawType, ResolvedType[] typeParams) + throws IllegalArgumentException + { + final TypeVariable[] vars = rawType.getTypeParameters(); + TypeBindings bindings = null; + for (int i = 0; i < vars.length; ++i) { + final Class actual = typeParams[i].getErasedType(); + for (Type bound : vars[i].getBounds()) { + if (bound == Object.class) { + continue; + } + if (bindings == null) { + bindings = TypeBindings.create(rawType, typeParams); + } + ResolvedType resolvedBound = _fromAny(null, bound, bindings); + if (!resolvedBound.getErasedType().isAssignableFrom(actual)) { + throw new IllegalArgumentException(String.format( +"Type parameter #%d/%d (%s) of %s does not satisfy bound `%s` of type variable `%s`", + i+1, vars.length, typeParams[i].getBriefDescription(), rawType.getName(), + resolvedBound.getBriefDescription(), vars[i].getName())); + } + } + } + } + /* /********************************************************************** /* Misc other methods @@ -655,6 +698,13 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act, Class s subtype.getTypeParameters()[placeholder.ordinal()].getName(), subtype.getName(), prev.getBriefDescription(), exp.getBriefDescription())); } + // [classmate#127]: `Object` (which may come from wildcard or raw type) is compatible + // with anything; placeholders within (if any) are bound to `Object` as well + // (to be replaced by bounds of type variables) + if (_isJavaLangObject(exp)) { + _bindToObject(act); + return true; + } // [classmate#127]: raw self-reference needs to be resolved to its bounds to be // comparable with other types (but not with another self-reference) if (!(exp instanceof ResolvedRecursiveType)) { @@ -696,6 +746,26 @@ private static boolean _isJavaLangObject(ResolvedType type) { return type.getErasedType() == Object.class; } + /** + * Helper method for binding all unbound placeholders within given type + * (including type itself) to {@code java.lang.Object}. + */ + private static void _bindToObject(ResolvedType type) + { + if (type instanceof TypePlaceHolder) { + TypePlaceHolder placeholder = (TypePlaceHolder) type; + if (placeholder.actualType() == null) { + placeholder.actualType(sJavaLangObject); + } + } else if (type.isArray()) { + _bindToObject(type.getArrayElementType()); + } else if (TypeBindings.isContextual(type)) { + for (ResolvedType t : type.getTypeParameters()) { + _bindToObject(t); + } + } + } + /** * Helper method for resolving raw self-reference (one without type bindings, for * generic class) into type with type parameters bound to their bounds; same as @@ -722,13 +792,13 @@ private ResolvedType _resolveRawSelfReference(ResolvedType type) * which are valid as-is. * * @param enclosing Erased types of enclosing types (containing given type - * as type parameter); {@code null} if none + * as type parameter), used as a stack: must be restored before returning * * @return Type with self-references replaced; given type itself if it contains none * * @since 1.8 */ - private ResolvedType _resolveSelfReferences(ResolvedType type, ClassStack enclosing) + private ResolvedType _resolveSelfReferences(ResolvedType type, List> enclosing) { if (!TypeBindings.isContextual(type)) { return type; @@ -740,18 +810,19 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, ClassStack enclos } final Class raw = type.getErasedType(); final boolean selfRef = (type instanceof ResolvedRecursiveType); - if (selfRef && (enclosing != null) && (enclosing.find(raw) != null)) { + if (selfRef && enclosing.contains(raw)) { return type; } // Self-reference itself is not an enclosing type for its bindings // (since it is replaced); otherwise it is - ClassStack nextEnclosing = selfRef ? enclosing - : ((enclosing == null) ? new ClassStack(raw) : enclosing.child(raw)); + if (!selfRef) { + enclosing.add(raw); + } final TypeBindings bindings = type.getTypeBindings(); ResolvedType[] newTypes = null; for (int i = 0, len = bindings.size(); i < len; ++i) { ResolvedType t = bindings.getBoundType(i); - ResolvedType newT = _resolveSelfReferences(t, nextEnclosing); + ResolvedType newT = _resolveSelfReferences(t, enclosing); if (newT != t) { if (newTypes == null) { newTypes = bindings.getTypeParameters().toArray(new ResolvedType[0]); @@ -759,6 +830,9 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, ClassStack enclos newTypes[i] = newT; } } + if (!selfRef) { + enclosing.remove(enclosing.size() - 1); + } if (newTypes != null) { return _fromClass(null, raw, TypeBindings.create(raw, newTypes)); } diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index fec70f4..138c647 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -72,6 +72,14 @@ static class NPair { } static class NSamePair extends NPair { } + static class NumArrayWrapper extends Wrapper { } + + static class ComparableNumWrapper> extends Wrapper { } + + static class BuilderWrapper> extends Wrapper { } + + static class MyBuilder extends Builder { } + abstract static class OuterType extends AbstractMap> { public abstract class Inner extends AbstractMap> { @@ -261,6 +269,43 @@ public void testSubtypeSameAsSupertypeWithSelfReference() RawSelfArray.class); } + // [classmate#127]: raw/wildcard supertype can be refined into array-binding subtype; + // unbound type variables resolved to their bounds + public void testSubtypeOfRawOrWildcardIntoArray() + { + for (ResolvedType supertype : new ResolvedType[] { + typeResolver.resolve(Wrapper.class), + typeResolver.resolve(new GenericType>() { }) + }) { + assertEquals(typeResolver.resolve(ArrayWrapper.class, Object.class), + typeResolver.resolveSubtype(supertype, ArrayWrapper.class)); + assertEquals(typeResolver.resolve(NumArrayWrapper.class, Number.class), + typeResolver.resolveSubtype(supertype, NumArrayWrapper.class)); + assertEquals(typeResolver.resolve(ListOfArrayWrapper.class, Object.class), + typeResolver.resolveSubtype(supertype, ListOfArrayWrapper.class)); + assertSame(IntListArrayWrapper.class, + typeResolver.resolveSubtype(supertype, IntListArrayWrapper.class).getErasedType()); + } + // and same for nested wildcard + ResolvedType supertype = typeResolver.resolve(new GenericType>>() { }); + assertEquals(typeResolver.resolve(ListWrapper.class, Object.class), + typeResolver.resolveSubtype(supertype, ListWrapper.class)); + } + + // [classmate#127]: type parameters must satisfy bounds of type variables + public void testSubtypeSatisfyingBounds() + { + assertEquals(typeResolver.resolve(NumArrayWrapper.class, Integer.class), + typeResolver.resolveSubtype(typeResolver.resolve(Wrapper.class, Integer[].class), + NumArrayWrapper.class)); + assertEquals(typeResolver.resolve(ComparableNumWrapper.class, Long.class), + typeResolver.resolveSubtype(typeResolver.resolve(Wrapper.class, Long.class), + ComparableNumWrapper.class)); + assertEquals(typeResolver.resolve(BuilderWrapper.class, MyBuilder.class), + typeResolver.resolveSubtype(typeResolver.resolve(Wrapper.class, MyBuilder.class), + BuilderWrapper.class)); + } + private void _verifyNoSelfReference(ResolvedType type, Class expRaw) { assertFalse(TypeResolver.isSelfReference(type)); @@ -575,4 +620,32 @@ public void testRawSelfReferenceVerifiedAsRaw() _verifyIncompatible(supertype, RawSelfSub.class); _verifyIncompatible(supertype, RawSelfOther.class); } + + // [classmate#127]: type parameters must satisfy bounds of type variables + public void testSubtypeViolatingBounds() + { + _verifyBoundViolation(typeResolver.resolve(Wrapper.class, String[].class), + NumArrayWrapper.class, "(java.lang.String)", "`java.lang.Number` of type variable `E`"); + // second bound + _verifyBoundViolation(typeResolver.resolve(Wrapper.class, java.util.concurrent.atomic.AtomicLong.class), + ComparableNumWrapper.class, "(java.util.concurrent.atomic.AtomicLong)", + "`java.lang.Comparable` of type variable `E`"); + _verifyBoundViolation(typeResolver.resolve(Wrapper.class, String.class), + BuilderWrapper.class, "(java.lang.String)", "of type variable `B`"); + } + + private void _verifyBoundViolation(ResolvedType supertype, Class subtype, + String... matches) + { + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, subtype); + fail("Expected failure, got: "+t.getFullDescription()); + } catch (IllegalArgumentException e) { + verifyException(e, "Type parameter #1/1"); + verifyException(e, "does not satisfy bound"); + for (String match : matches) { + verifyException(e, match); + } + } + } } From 130106fac02390619f4dfa614497cdb309870cf9 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 8 Oct 2026 18:52:27 -0700 Subject: [PATCH 07/11] Yet more twiddling --- VERSION.txt | 7 +- .../com/fasterxml/classmate/TypeResolver.java | 90 ++++++++----------- .../classmate/TestSubtypeResolution.java | 51 +++++++---- 3 files changed, 74 insertions(+), 74 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 51bc3d9..a0ee2a6 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -31,12 +31,9 @@ Release notes: longer included in types returned by `resolveSubtype()`; raw self-references resolve to their bounds (self-references to enclosing type, like in raw `Enum`, are retained as-is) -- `resolveSubtype()` now accepts `Object` (from wildcard or raw type, like raw - `Wrapper` or `Wrapper`) as compatible with any type parameter of subtype - (like `E[]` of `ArrayWrapper extends Wrapper`); type variables bound only - this way resolve to their bounds (`ArrayWrapper`) - `resolveSubtype()` now verifies that type parameters of subtype satisfy (type-erased) - bounds of its type variables (f.ex rejects `NumArray` with `E` of `String`) + bounds of its type variables (f.ex rejects `NumArray` with `E` of `String`); + type parameters of `Object` (which may come from wildcard or raw type) are not verified - `TypeResolver.resolve(Type, Type...)` now fails with `IllegalArgumentException` if any of type parameters is a primitive type (like `resolve(List.class, int.class)`) (fix by @cowtowncoder w/ Claude code) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 1a830f7..5b478c1 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -247,9 +247,7 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp // first: [classmate#127] replace self-references only valid within their original // resolution context (like supertype itself being one, or one nested within supertype // obtained from `getParentClass()`) with stand-alone types - if (TypeBindings.isContextual(supertype)) { - supertype = _resolveSelfReferences(supertype, new ArrayList<>()); - } + supertype = _resolveSelfReferences(supertype, new ArrayList<>()); // Then, trivial check for case where subtype is supertype... final Class superclass = supertype.getErasedType(); if (superclass == subtype) { // unlikely but cheap check so let's just do it @@ -298,9 +296,6 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp } // but with type parameters, need to reconstruct final ResolvedType[] typeParams = new ResolvedType[paramCount]; - // [classmate#127]: `Object` (from wildcard or raw type) to be replaced by bound - // of type variable; same as for raw subtype - List rawParams = null; for (int i = 0; i < paramCount; ++i) { ResolvedType t = placeholders[i].actualType(); // Is it ok for it to be left unassigned? For now let's not allow that @@ -311,12 +306,6 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp throw new IllegalArgumentException("Failed to find type parameter #"+(i+1)+"/" +paramCount+" for "+subtype.getName()); } - if (_isJavaLangObject(t)) { - if (rawParams == null) { - rawParams = _fromClass(null, subtype, TypeBindings.emptyBindings()).getTypeParameters(); - } - t = rawParams.get(i); - } typeParams[i] = t; } _verifyBounds(subtype, typeParams); @@ -326,32 +315,51 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp /** * Helper method for verifying that given type parameters satisfy declared bounds * of type variables of given type (as far as type-erased types are concerned). + * Type parameters of {@code java.lang.Object} are not verified, since they may + * come from wildcard or raw type. * * @since 1.8 */ - private void _verifyBounds(Class rawType, ResolvedType[] typeParams) + private static void _verifyBounds(Class rawType, ResolvedType[] typeParams) throws IllegalArgumentException { final TypeVariable[] vars = rawType.getTypeParameters(); - TypeBindings bindings = null; for (int i = 0; i < vars.length; ++i) { - final Class actual = typeParams[i].getErasedType(); + final ResolvedType param = typeParams[i]; + if (_isJavaLangObject(param)) { + continue; + } for (Type bound : vars[i].getBounds()) { - if (bound == Object.class) { - continue; - } - if (bindings == null) { - bindings = TypeBindings.create(rawType, typeParams); - } - ResolvedType resolvedBound = _fromAny(null, bound, bindings); - if (!resolvedBound.getErasedType().isAssignableFrom(actual)) { + Class erasedBound = _erasedBound(bound, vars, typeParams); + if ((erasedBound != null) && !erasedBound.isAssignableFrom(param.getErasedType())) { throw new IllegalArgumentException(String.format( "Type parameter #%d/%d (%s) of %s does not satisfy bound `%s` of type variable `%s`", - i+1, vars.length, typeParams[i].getBriefDescription(), rawType.getName(), - resolvedBound.getBriefDescription(), vars[i].getName())); + i+1, vars.length, param.getBriefDescription(), rawType.getName(), + bound.getTypeName(), vars[i].getName())); + } + } + } + } + + /** + * @return Type-erased bound, if one can be determined; {@code null} if not + */ + private static Class _erasedBound(Type bound, TypeVariable[] vars, ResolvedType[] typeParams) + { + if (bound instanceof Class) { + return (Class) bound; + } + if (bound instanceof ParameterizedType) { + return (Class) ((ParameterizedType) bound).getRawType(); + } + if (bound instanceof TypeVariable) { // like `V extends K` + for (int i = 0; i < vars.length; ++i) { + if (vars[i].equals(bound)) { + return typeParams[i].getErasedType(); } } } + return null; } /* @@ -677,6 +685,11 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act, Class s if (exp.isPrimitive()) { return false; } + // [classmate#127]: self-reference retained within its enclosing type is + // not valid as a stand-alone type parameter + if (exp instanceof ResolvedRecursiveType) { + exp = _resolveSelfReferences(exp, new ArrayList<>()); + } TypePlaceHolder placeholder = (TypePlaceHolder) act; ResolvedType prev = placeholder.actualType(); if (prev == null) { @@ -698,13 +711,6 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act, Class s subtype.getTypeParameters()[placeholder.ordinal()].getName(), subtype.getName(), prev.getBriefDescription(), exp.getBriefDescription())); } - // [classmate#127]: `Object` (which may come from wildcard or raw type) is compatible - // with anything; placeholders within (if any) are bound to `Object` as well - // (to be replaced by bounds of type variables) - if (_isJavaLangObject(exp)) { - _bindToObject(act); - return true; - } // [classmate#127]: raw self-reference needs to be resolved to its bounds to be // comparable with other types (but not with another self-reference) if (!(exp instanceof ResolvedRecursiveType)) { @@ -746,26 +752,6 @@ private static boolean _isJavaLangObject(ResolvedType type) { return type.getErasedType() == Object.class; } - /** - * Helper method for binding all unbound placeholders within given type - * (including type itself) to {@code java.lang.Object}. - */ - private static void _bindToObject(ResolvedType type) - { - if (type instanceof TypePlaceHolder) { - TypePlaceHolder placeholder = (TypePlaceHolder) type; - if (placeholder.actualType() == null) { - placeholder.actualType(sJavaLangObject); - } - } else if (type.isArray()) { - _bindToObject(type.getArrayElementType()); - } else if (TypeBindings.isContextual(type)) { - for (ResolvedType t : type.getTypeParameters()) { - _bindToObject(t); - } - } - } - /** * Helper method for resolving raw self-reference (one without type bindings, for * generic class) into type with type parameters bound to their bounds; same as diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index 138c647..9cc0807 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -80,6 +80,10 @@ static class BuilderWrapper> extends Wrapper { } static class MyBuilder extends Builder { } + static class KVPair extends Pair { } + + static class EnumHolder> extends Wrapper> { } + abstract static class OuterType extends AbstractMap> { public abstract class Inner extends AbstractMap> { @@ -269,27 +273,34 @@ public void testSubtypeSameAsSupertypeWithSelfReference() RawSelfArray.class); } - // [classmate#127]: raw/wildcard supertype can be refined into array-binding subtype; - // unbound type variables resolved to their bounds - public void testSubtypeOfRawOrWildcardIntoArray() + // [classmate#127]: `Object` type parameters (possibly from wildcard or raw type) + // are not verified against bounds + public void testSubtypeWithObjectNotVerifiedAgainstBounds() { - for (ResolvedType supertype : new ResolvedType[] { + // `V extends K` + ResolvedType supertype = typeResolver.resolve(new GenericType>() { }); + assertEquals(typeResolver.resolve(KVPair.class, String.class, Object.class), + typeResolver.resolveSubtype(supertype, KVPair.class)); + // multiple bounds + for (ResolvedType wrapper : new ResolvedType[] { typeResolver.resolve(Wrapper.class), typeResolver.resolve(new GenericType>() { }) }) { - assertEquals(typeResolver.resolve(ArrayWrapper.class, Object.class), - typeResolver.resolveSubtype(supertype, ArrayWrapper.class)); - assertEquals(typeResolver.resolve(NumArrayWrapper.class, Number.class), - typeResolver.resolveSubtype(supertype, NumArrayWrapper.class)); - assertEquals(typeResolver.resolve(ListOfArrayWrapper.class, Object.class), - typeResolver.resolveSubtype(supertype, ListOfArrayWrapper.class)); - assertSame(IntListArrayWrapper.class, - typeResolver.resolveSubtype(supertype, IntListArrayWrapper.class).getErasedType()); + assertEquals(typeResolver.resolve(ComparableNumWrapper.class, Object.class), + typeResolver.resolveSubtype(wrapper, ComparableNumWrapper.class)); } - // and same for nested wildcard - ResolvedType supertype = typeResolver.resolve(new GenericType>>() { }); - assertEquals(typeResolver.resolve(ListWrapper.class, Object.class), - typeResolver.resolveSubtype(supertype, ListWrapper.class)); + } + + // [classmate#127]: self-reference (valid within enclosing type) must not become + // stand-alone type parameter of subtype + public void testSubtypeBindingSelfReference() + { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, Enum.class); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, EnumHolder.class); + ResolvedType param = subtype.getTypeParameters().get(0); + assertFalse(TypeResolver.isSelfReference(param)); + assertSame(Enum.class, param.getErasedType()); + assertNotNull(param.getParentClass()); } // [classmate#127]: type parameters must satisfy bounds of type variables @@ -629,7 +640,7 @@ public void testSubtypeViolatingBounds() // second bound _verifyBoundViolation(typeResolver.resolve(Wrapper.class, java.util.concurrent.atomic.AtomicLong.class), ComparableNumWrapper.class, "(java.util.concurrent.atomic.AtomicLong)", - "`java.lang.Comparable` of type variable `E`"); + "`java.lang.Comparable` of type variable `E`"); _verifyBoundViolation(typeResolver.resolve(Wrapper.class, String.class), BuilderWrapper.class, "(java.lang.String)", "of type variable `B`"); } @@ -648,4 +659,10 @@ private void _verifyBoundViolation(ResolvedType supertype, Class subtype, } } } + + // [classmate#127]: `Object[]` is not compatible with primitive array + public void testObjectArrayNotCompatibleWithPrimitiveArray() + { + _verifyIncompatible(typeResolver.resolve(Wrapper.class, Object[].class), IntArrayWrapper.class); + } } From 27eb37a2835b9a74cb8b75d8cb32efbea848dccb Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 8 Oct 2026 18:58:58 -0700 Subject: [PATCH 08/11] tiny streamlining --- src/main/java/com/fasterxml/classmate/TypeResolver.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 5b478c1..37312f8 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -165,13 +165,14 @@ public ResolvedType resolve(Type type, Type... typeParameters) int len = typeParameters.length; ResolvedType[] resolvedParams = new ResolvedType[len]; for (int i = 0; i < len; ++i) { - resolvedParams[i] = _fromAny(null, typeParameters[i], bindings); + final ResolvedType param = _fromAny(null, typeParameters[i], bindings); // [classmate#127]: primitive types are not valid type parameters - if (resolvedParams[i].isPrimitive()) { + if (param.isPrimitive()) { throw new IllegalArgumentException("Can not use primitive type (" - +resolvedParams[i].getBriefDescription()+") as type parameter #" + +param.getBriefDescription()+") as type parameter #" +(i+1)+"/"+len+" of "+rawBase.getName()); } + resolvedParams[i] = param; } return _fromClass(null, rawBase, TypeBindings.create(rawBase, resolvedParams)); } From 05ab1872d7679e6d0c614d4dd32da2558f046a2b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 11:02:40 -0700 Subject: [PATCH 09/11] More fixes --- VERSION.txt | 14 +- .../com/fasterxml/classmate/TypeResolver.java | 269 +++++++++++------- .../classmate/types/TypePlaceHolder.java | 7 - .../classmate/TestSubtypeResolution.java | 122 +++++--- .../fasterxml/classmate/TypeResolverTest.java | 10 +- 5 files changed, 257 insertions(+), 165 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index a0ee2a6..fed6dd0 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -24,16 +24,14 @@ Release notes: #127: `resolveSubtype()` does not verify or resolve array element types - `resolveSubtype()` now rejects type variable bound to conflicting types (like `E` in `Dup extends Pair`; except that `Object`, which may - come from wildcard or raw type, is compatible with more specific type) and - primitive types bound to type variables (like `int` for `E` in `E[]`) + come from wildcard or raw type, is compatible with more specific type at any + level of nesting, like `List` with `List`) and primitive types bound + to type variables (like `int` for `E` in `E[]`) - Self-references (`ResolvedRecursiveType`) only valid within their original resolution context (like ones in type parameters of `getParentClass()`) are no - longer included in types returned by `resolveSubtype()`; raw self-references - resolve to their bounds (self-references to enclosing type, like in raw `Enum`, - are retained as-is) -- `resolveSubtype()` now verifies that type parameters of subtype satisfy (type-erased) - bounds of its type variables (f.ex rejects `NumArray` with `E` of `String`); - type parameters of `Object` (which may come from wildcard or raw type) are not verified + longer included in types returned by `resolveSubtype()`: raw self-references + resolve to their bounds, others to the referenced type (self-references to + enclosing type, like in raw `Enum`, are retained as-is) - `TypeResolver.resolve(Type, Type...)` now fails with `IllegalArgumentException` if any of type parameters is a primitive type (like `resolve(List.class, int.class)`) (fix by @cowtowncoder w/ Claude code) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 37312f8..0de59fe 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -248,7 +248,7 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp // first: [classmate#127] replace self-references only valid within their original // resolution context (like supertype itself being one, or one nested within supertype // obtained from `getParentClass()`) with stand-alone types - supertype = _resolveSelfReferences(supertype, new ArrayList<>()); + supertype = _resolveSelfReferences(supertype, null); // Then, trivial check for case where subtype is supertype... final Class superclass = supertype.getErasedType(); if (superclass == subtype) { // unlikely but cheap check so let's just do it @@ -290,7 +290,7 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp } // Ok, then, let's find and verify type assignments; resolve type holders if any // (and yes, even for no-type-parameters case) - _resolveTypePlaceholders(supertype, resolvedSupertype, subtype); + _resolveTypePlaceholders(supertype, resolvedSupertype, subtype, placeholders); // And then re-construct, if necessary if (paramCount == 0) { // if no type parameters, fine as is return resolvedSubtype; @@ -309,60 +309,9 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp } typeParams[i] = t; } - _verifyBounds(subtype, typeParams); return resolve(subtype, typeParams); } - /** - * Helper method for verifying that given type parameters satisfy declared bounds - * of type variables of given type (as far as type-erased types are concerned). - * Type parameters of {@code java.lang.Object} are not verified, since they may - * come from wildcard or raw type. - * - * @since 1.8 - */ - private static void _verifyBounds(Class rawType, ResolvedType[] typeParams) - throws IllegalArgumentException - { - final TypeVariable[] vars = rawType.getTypeParameters(); - for (int i = 0; i < vars.length; ++i) { - final ResolvedType param = typeParams[i]; - if (_isJavaLangObject(param)) { - continue; - } - for (Type bound : vars[i].getBounds()) { - Class erasedBound = _erasedBound(bound, vars, typeParams); - if ((erasedBound != null) && !erasedBound.isAssignableFrom(param.getErasedType())) { - throw new IllegalArgumentException(String.format( -"Type parameter #%d/%d (%s) of %s does not satisfy bound `%s` of type variable `%s`", - i+1, vars.length, param.getBriefDescription(), rawType.getName(), - bound.getTypeName(), vars[i].getName())); - } - } - } - } - - /** - * @return Type-erased bound, if one can be determined; {@code null} if not - */ - private static Class _erasedBound(Type bound, TypeVariable[] vars, ResolvedType[] typeParams) - { - if (bound instanceof Class) { - return (Class) bound; - } - if (bound instanceof ParameterizedType) { - return (Class) ((ParameterizedType) bound).getRawType(); - } - if (bound instanceof TypeVariable) { // like `V extends K` - for (int i = 0; i < vars.length; ++i) { - if (vars[i].equals(bound)) { - return typeParams[i].getErasedType(); - } - } - } - return null; - } - /* /********************************************************************** /* Misc other methods @@ -656,9 +605,11 @@ private ResolvedType _fromVariable(ClassStack context, TypeVariable variable, * * @param sourceType Original base type used for specification/refinement * @param actualType Base type instance after re-resolving, possibly containing type placeholders + * @param subtype Type-erased subtype being resolved (for error messages) + * @param placeholders Placeholders for type parameters of subtype, if any (for error messages) */ private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actualType, - Class subtype) + Class subtype, TypePlaceHolder[] placeholders) throws IllegalArgumentException { List expectedTypes = sourceType.getTypeParameters(); @@ -666,10 +617,19 @@ private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actu for (int i = 0, len = expectedTypes.size(); i < len; ++i) { ResolvedType exp = expectedTypes.get(i); ResolvedType act = actualTypes.get(i); - if (!_verifyAndResolve(exp, act, subtype)) { - throw new IllegalArgumentException("Type parameter #"+(i+1)+"/"+len+" differs; expected " - +exp.getBriefDescription()+", got "+act.getBriefDescription()); + String msg; + try { + if (_verifyAndResolve(exp, act)) { + continue; + } + msg = "expected "+exp.getBriefDescription()+", got "+act.getBriefDescription(); + } catch (BindingConflict e) { + msg = String.format("conflicting bindings for type variable `%s` of %s: %s vs %s", + subtype.getTypeParameters()[Arrays.asList(placeholders).indexOf(e.placeholder)].getName(), + subtype.getName(), + e.placeholder.actualType().getBriefDescription(), e.type.getBriefDescription()); } + throw new IllegalArgumentException("Type parameter #"+(i+1)+"/"+len+" differs; "+msg); } } @@ -677,8 +637,10 @@ private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actu * @param exp Expected type, from supertype being refined (with all self-references * not valid outside their resolution context already replaced) * @param act Actual type, from re-resolved subtype; may contain placeholders + * + * @throws BindingConflict If a placeholder is already bound to an incompatible type */ - private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act, Class subtype) + private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) { // See if we have an actual type placeholder to resolve; if yes, replace if (act instanceof TypePlaceHolder) { @@ -686,40 +648,31 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act, Class s if (exp.isPrimitive()) { return false; } - // [classmate#127]: self-reference retained within its enclosing type is - // not valid as a stand-alone type parameter - if (exp instanceof ResolvedRecursiveType) { - exp = _resolveSelfReferences(exp, new ArrayList<>()); - } + // [classmate#127]: self-references to types enclosing `exp` (if any) are + // not valid outside of it + exp = _resolveSelfReferences(exp, null); TypePlaceHolder placeholder = (TypePlaceHolder) act; ResolvedType prev = placeholder.actualType(); if (prev == null) { placeholder.actualType(exp); return true; } - // [classmate#127]: if already bound, must be bound to the same type; except - // that `Object` (which may come from wildcard or raw type) is compatible with - // anything, in which case the more specific type is retained - if (prev.equals(exp) || _isJavaLangObject(exp)) { - return true; + // [classmate#127]: if already bound, must be bound to compatible type + ResolvedType merged = _mergeBindings(prev, exp); + if (merged == null) { + throw new BindingConflict(placeholder, exp); } - if (_isJavaLangObject(prev)) { - placeholder.actualType(exp); - return true; - } - throw new IllegalArgumentException(String.format( -"Conflicting bindings for type variable `%s` of %s: %s vs %s", - subtype.getTypeParameters()[placeholder.ordinal()].getName(), subtype.getName(), - prev.getBriefDescription(), exp.getBriefDescription())); - } - // [classmate#127]: raw self-reference needs to be resolved to its bounds to be - // comparable with other types (but not with another self-reference) - if (!(exp instanceof ResolvedRecursiveType)) { + placeholder.actualType(merged); + return true; + } + // [classmate#127]: self-reference needs to be resolved to be comparable with + // other types (but not with another self-reference) + if (isSelfReference(exp) && !isSelfReference(act)) { + exp = _selfReferenceTarget(exp); + } else if (isSelfReference(act) && !isSelfReference(exp)) { + // but only raw one: others have bindings (possibly with placeholders) to use act = _resolveRawSelfReference(act); } - if (!(act instanceof ResolvedRecursiveType)) { - exp = _resolveRawSelfReference(exp); - } // [classmate#127]: Array types have no type parameters, so need to verify // (and resolve) element types instead. Must be done before erased type check // since array of placeholder has erased type of `Object[]` @@ -727,7 +680,7 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act, Class s return false; } if (exp.isArray()) { - return _verifyAndResolve(exp.getArrayElementType(), act.getArrayElementType(), subtype); + return _verifyAndResolve(exp.getArrayElementType(), act.getArrayElementType()); } // if not, try to verify compatibility. But note that we can not // use simple equality as we need to resolve recursively @@ -742,13 +695,80 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act, Class s return false; } for (int i = 0; i < len; ++i) { - if (!_verifyAndResolve(expectedTypes.get(i), actualTypes.get(i), subtype)) { + if (!_verifyAndResolve(expectedTypes.get(i), actualTypes.get(i))) { return false; } } return true; } + /** + * Helper method for merging two bindings of the same type variable: they are + * compatible if they are equal, except that {@code java.lang.Object} (which may come + * from wildcard or raw type) is compatible with any non-primitive type, at any level + * of nesting. If so, the more specific type is returned. + * + * @return Merged type, if types are compatible; {@code null} if not + * + * @since 1.8 + */ + private ResolvedType _mergeBindings(ResolvedType a, ResolvedType b) + { + if (a.equals(b)) { + return a; + } + if (_isJavaLangObject(a) && !b.isPrimitive()) { + return b; + } + if (_isJavaLangObject(b) && !a.isPrimitive()) { + return a; + } + if (isSelfReference(a) || isSelfReference(b) || (a.isArray() != b.isArray())) { + return null; + } + if (a.isArray()) { + ResolvedType elemA = a.getArrayElementType(); + ResolvedType elemB = b.getArrayElementType(); + ResolvedType elem = _mergeBindings(elemA, elemB); + if (elem == null) { + return null; + } + if (elem == elemA) { + return a; + } + if (elem == elemB) { + return b; + } + return _arrayOf(_arrayClassFor(elem), elem); + } + if (a.getErasedType() != b.getErasedType()) { + return null; + } + final List paramsA = a.getTypeParameters(); + final List paramsB = b.getTypeParameters(); + final int len = paramsA.size(); + if (len != paramsB.size()) { + return null; + } + ResolvedType[] merged = new ResolvedType[len]; + boolean sameAsA = true, sameAsB = true; + for (int i = 0; i < len; ++i) { + merged[i] = _mergeBindings(paramsA.get(i), paramsB.get(i)); + if (merged[i] == null) { + return null; + } + sameAsA &= (merged[i] == paramsA.get(i)); + sameAsB &= (merged[i] == paramsB.get(i)); + } + if (sameAsA) { + return a; + } + if (sameAsB) { + return b; + } + return _fromClass(null, a.getErasedType(), TypeBindings.create(a.getErasedType(), merged)); + } + private static boolean _isJavaLangObject(ResolvedType type) { return type.getErasedType() == Object.class; } @@ -762,7 +782,7 @@ private static boolean _isJavaLangObject(ResolvedType type) { */ private ResolvedType _resolveRawSelfReference(ResolvedType type) { - if ((type instanceof ResolvedRecursiveType) + if (isSelfReference(type) && type.getTypeBindings().isEmpty() && type.getErasedType().getTypeParameters().length > 0) { return _fromClass(null, type.getErasedType(), type.getTypeBindings()); @@ -770,6 +790,26 @@ private ResolvedType _resolveRawSelfReference(ResolvedType type) return type; } + /** + * Helper method for finding stand-alone type to use in place of given self-reference: + * raw one is resolved with type parameters bound to their bounds; others to the type + * referenced. + * + * @since 1.8 + */ + private ResolvedType _selfReferenceTarget(ResolvedType selfRef) + { + ResolvedType raw = _resolveRawSelfReference(selfRef); + if (raw != selfRef) { + return raw; + } + ResolvedType ref = selfRef.getSelfReferencedType(); + if (ref != null) { + return ref; + } + return _fromClass(null, selfRef.getErasedType(), selfRef.getTypeBindings()); + } + /** * Helper method for replacing self-references ({@link ResolvedRecursiveType}s) * that are not valid outside their original resolution context with stand-alone @@ -778,14 +818,14 @@ private ResolvedType _resolveRawSelfReference(ResolvedType type) * self-references to an enclosing type (like {@code E} in {@code Enum>}) * which are valid as-is. * - * @param enclosing Erased types of enclosing types (containing given type - * as type parameter), used as a stack: must be restored before returning + * @param enclosing Enclosing types (containing given type as type parameter), used as + * a stack (must be restored before returning); {@code null} if none * * @return Type with self-references replaced; given type itself if it contains none * * @since 1.8 */ - private ResolvedType _resolveSelfReferences(ResolvedType type, List> enclosing) + private ResolvedType _resolveSelfReferences(ResolvedType type, List enclosing) { if (!TypeBindings.isContextual(type)) { return type; @@ -795,16 +835,22 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List> en ResolvedType newElem = _resolveSelfReferences(elem, enclosing); return (newElem == elem) ? type : _arrayOf(type.getErasedType(), newElem); } - final Class raw = type.getErasedType(); - final boolean selfRef = (type instanceof ResolvedRecursiveType); - if (selfRef && enclosing.contains(raw)) { - return type; + if (isSelfReference(type)) { + // Self-reference to an enclosing type (by identity) is valid as-is + if (enclosing != null) { + final ResolvedType ref = type.getSelfReferencedType(); + for (ResolvedType t : enclosing) { + if (t == ref) { + return type; + } + } + } + return _selfReferenceTarget(type); } - // Self-reference itself is not an enclosing type for its bindings - // (since it is replaced); otherwise it is - if (!selfRef) { - enclosing.add(raw); + if (enclosing == null) { + enclosing = new ArrayList<>(); } + enclosing.add(type); final TypeBindings bindings = type.getTypeBindings(); ResolvedType[] newTypes = null; for (int i = 0, len = bindings.size(); i < len; ++i) { @@ -817,15 +863,28 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List> en newTypes[i] = newT; } } - if (!selfRef) { - enclosing.remove(enclosing.size() - 1); - } - if (newTypes != null) { - return _fromClass(null, raw, TypeBindings.create(raw, newTypes)); + enclosing.remove(enclosing.size() - 1); + if (newTypes == null) { + return type; } - if (selfRef) { - return _fromClass(null, raw, bindings); + final Class raw = type.getErasedType(); + return _fromClass(null, raw, TypeBindings.create(raw, newTypes)); + } + + /** + * Exception used to indicate that a type placeholder (type variable of subtype) + * would need to be bound to two incompatible types. + */ + @SuppressWarnings("serial") + private static final class BindingConflict extends RuntimeException + { + final TypePlaceHolder placeholder; + final ResolvedType type; + + BindingConflict(TypePlaceHolder placeholder, ResolvedType type) { + super(null, null, false, false); + this.placeholder = placeholder; + this.type = type; } - return type; } } diff --git a/src/main/java/com/fasterxml/classmate/types/TypePlaceHolder.java b/src/main/java/com/fasterxml/classmate/types/TypePlaceHolder.java index ec3b286..a441e96 100644 --- a/src/main/java/com/fasterxml/classmate/types/TypePlaceHolder.java +++ b/src/main/java/com/fasterxml/classmate/types/TypePlaceHolder.java @@ -28,13 +28,6 @@ public TypePlaceHolder(int ordinal) @Override public boolean canCreateSubtypes() { return false; } - /** - * @return Index of type parameter (of the type being resolved) this placeholder is for - * - * @since 1.8 - */ - public int ordinal() { return _ordinal; } - public ResolvedType actualType() { return _actualType; } public void actualType(ResolvedType t) { _actualType = t; } diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index 9cc0807..fb788d7 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -62,9 +62,11 @@ static class SelfInList extends Wrapper> { } static class SamePair extends Pair { } - static class SelfParam extends Wrapper> { } + static class RecInList extends Wrapper>> { } - static class SelfParamSub extends SelfParam { } + static class EnumPair> extends Pair, E> { } + + static class IntOnlyWrapper extends Wrapper { } static class Builder> { } @@ -224,15 +226,45 @@ public void testSubtypeWithSelfReferenceAndRepeatedTypeVariable() assertEquals(withoutSelfRef, subtype.getTypeParameters().get(0)); } - // [classmate#127]: self-reference as supertype is resolved using its own bindings + // [classmate#127]: self-reference as supertype is resolved to referenced type public void testSubtypeOfSelfReference() { - ResolvedType supertype = typeResolver.resolve(SelfParam.class, Integer.class) - .getParentClass().getTypeParameters().get(0); + ResolvedType rawEnum = typeResolver.resolve(Enum.class); + ResolvedType supertype = rawEnum.getTypeParameters().get(0); assertTrue(TypeResolver.isSelfReference(supertype)); - ResolvedType subtype = typeResolver.resolveSubtype(supertype, SelfParamSub.class); - assertSame(SelfParamSub.class, subtype.getErasedType()); - assertSame(String.class, subtype.getTypeParameters().get(0).getErasedType()); + assertEquals(rawEnum, typeResolver.resolveSubtype(supertype, Enum.class)); + } + + // [classmate#127]: self-reference to another instance of the same class (from + // different resolution context) must not be retained + public void testSubtypeWithSelfReferenceFromOtherContext() + { + ResolvedType listOfSelfRef = typeResolver.resolve(RecInList.class, String.class) + .getParentClass().getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(listOfSelfRef.getTypeParameters().get(0))); + ResolvedType supertype = typeResolver.resolve(Wrapper.class, + typeResolver.resolve(RecInList.class, listOfSelfRef)); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, SubWrapper.class); + // SubWrapper>>> + ResolvedType inner = subtype.getTypeParameters().get(0) // RecInList<...> + .getTypeParameters().get(0) // List<...> + .getTypeParameters().get(0); // RecInList + assertFalse(TypeResolver.isSelfReference(inner)); + assertEquals(typeResolver.resolve(RecInList.class, String.class), inner); + } + + // [classmate#127]: raw `Enum` (with self-reference) bound consistently + public void testSubtypeWithRawEnumAndRepeatedTypeVariable() + { + ResolvedType rawEnum = typeResolver.resolve(Enum.class); + ResolvedType subtype = typeResolver.resolveSubtype( + typeResolver.resolve(Pair.class, Enum.class, Enum.class), EnumPair.class); + assertEquals(rawEnum, subtype.getTypeParameters().get(0)); + + subtype = typeResolver.resolveSubtype( + typeResolver.resolve(Pair.class, rawEnum, rawEnum.getTypeParameters().get(0)), + SamePair.class); + assertEquals(rawEnum, subtype.getTypeParameters().get(0)); } // [classmate#127]: self-references valid as-is (like in raw `Enum`) must be retained @@ -299,11 +331,44 @@ public void testSubtypeBindingSelfReference() ResolvedType subtype = typeResolver.resolveSubtype(supertype, EnumHolder.class); ResolvedType param = subtype.getTypeParameters().get(0); assertFalse(TypeResolver.isSelfReference(param)); - assertSame(Enum.class, param.getErasedType()); + assertEquals(typeResolver.resolve(Enum.class), param); assertNotNull(param.getParentClass()); } - // [classmate#127]: type parameters must satisfy bounds of type variables + // [classmate#127]: `Object` (from wildcard or raw type) nested within type + // compatible with more specific binding + public void testSubtypeWithNestedWildcardAndRepeatedTypeVariable() + { + ResolvedType listOfString = typeResolver.resolve(List.class, String.class); + ResolvedType supertype = typeResolver.resolve( + new GenericType, List>>() { }); + assertEquals(typeResolver.resolve(java.util.function.UnaryOperator.class, listOfString), + typeResolver.resolveSubtype(supertype, java.util.function.UnaryOperator.class)); + + supertype = typeResolver.resolve(Pair.class, List.class, listOfString); + assertEquals(listOfString, + typeResolver.resolveSubtype(supertype, SamePair.class).getTypeParameters().get(0)); + + supertype = typeResolver.resolve(Pair.class, Object[].class, String[].class); + assertEquals(typeResolver.resolve(String[].class), + typeResolver.resolveSubtype(supertype, SamePair.class).getTypeParameters().get(0)); + + // and merged from both + supertype = typeResolver.resolve(new GenericType, Map>>() { }); + assertEquals(typeResolver.resolve(Map.class, String.class, Integer.class), + typeResolver.resolveSubtype(supertype, SamePair.class).getTypeParameters().get(0)); + } + + // [classmate#127]: bounds of type variables are not verified (wildcard upper bound + // can not be distinguished from actual type; see [classmate#130]) + public void testSubtypeOfBoundedWildcard() + { + ResolvedType supertype = typeResolver.resolve(new GenericType>() { }); + assertSame(IntOnlyWrapper.class, + typeResolver.resolveSubtype(supertype, IntOnlyWrapper.class).getErasedType()); + } + + // [classmate#127]: bounded type variables bound to types satisfying bounds public void testSubtypeSatisfyingBounds() { assertEquals(typeResolver.resolve(NumArrayWrapper.class, Integer.class), @@ -608,9 +673,14 @@ public void testConflictingTypeVariableBindings() ResolvedType t = typeResolver.resolveSubtype(supertype, DupPair.class); fail("Expected failure, got: "+t.getFullDescription()); } catch (IllegalArgumentException e) { - verifyException(e, "Conflicting bindings for type variable `E` of "+DupPair.class.getName() - +": java.lang.String vs java.lang.Integer"); + verifyException(e, "Type parameter #2/2 differs; conflicting bindings for type variable `E` of " + +DupPair.class.getName()+": java.lang.String vs java.lang.Integer"); } + // also nested, and for primitive arrays + _verifyIncompatible(typeResolver.resolve(new GenericType, Map>>() { }), + SamePair.class); + _verifyIncompatible(typeResolver.resolve(Pair.class, Object[].class, int[].class), + SamePair.class); } private void _verifyIncompatible(ResolvedType supertype, Class subtype) @@ -632,34 +702,6 @@ public void testRawSelfReferenceVerifiedAsRaw() _verifyIncompatible(supertype, RawSelfOther.class); } - // [classmate#127]: type parameters must satisfy bounds of type variables - public void testSubtypeViolatingBounds() - { - _verifyBoundViolation(typeResolver.resolve(Wrapper.class, String[].class), - NumArrayWrapper.class, "(java.lang.String)", "`java.lang.Number` of type variable `E`"); - // second bound - _verifyBoundViolation(typeResolver.resolve(Wrapper.class, java.util.concurrent.atomic.AtomicLong.class), - ComparableNumWrapper.class, "(java.util.concurrent.atomic.AtomicLong)", - "`java.lang.Comparable` of type variable `E`"); - _verifyBoundViolation(typeResolver.resolve(Wrapper.class, String.class), - BuilderWrapper.class, "(java.lang.String)", "of type variable `B`"); - } - - private void _verifyBoundViolation(ResolvedType supertype, Class subtype, - String... matches) - { - try { - ResolvedType t = typeResolver.resolveSubtype(supertype, subtype); - fail("Expected failure, got: "+t.getFullDescription()); - } catch (IllegalArgumentException e) { - verifyException(e, "Type parameter #1/1"); - verifyException(e, "does not satisfy bound"); - for (String match : matches) { - verifyException(e, match); - } - } - } - // [classmate#127]: `Object[]` is not compatible with primitive array public void testObjectArrayNotCompatibleWithPrimitiveArray() { diff --git a/src/test/java/com/fasterxml/classmate/TypeResolverTest.java b/src/test/java/com/fasterxml/classmate/TypeResolverTest.java index f9b7b13..a5464d1 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolverTest.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolverTest.java @@ -508,21 +508,21 @@ public void testTypesMatch() throws IllegalAccessException, InvocationTargetExce // first test equality GenericType>> matchBList = new GenericType>>() { }; ResolvedType matchBListResolved = typeResolver.resolve(matchBList); - assertTrue((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchBListResolved, Object.class)); + assertTrue((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchBListResolved)); GenericType, Comparator>>> matchASet = new GenericType, Comparator>>>() { }; GenericType, Comparator>>> matchASet1 = new GenericType, Comparator>>>() { }; ResolvedType matchASetResolved = typeResolver.resolve(matchASet); ResolvedType matchASetResolved1 = typeResolver.resolve(matchASet1); - assertTrue((Boolean) typesMatchMethod.invoke(typeResolver, matchASetResolved, matchASetResolved1, Object.class)); + assertTrue((Boolean) typesMatchMethod.invoke(typeResolver, matchASetResolved, matchASetResolved1)); // now check inequality GenericType, Comparator>>> matchAList = new GenericType, Comparator>>>() { }; ResolvedType matchAListResolved = typeResolver.resolve(matchAList); - assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchASetResolved, Object.class)); + assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchASetResolved)); // now ensure different number of type-parameters are handled correctly - assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchBListResolved, Object.class)); - assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchAListResolved, Object.class)); + assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchBListResolved)); + assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchAListResolved)); } public void testMultiDimensionalGenericArrays() throws Exception From 0e8ae43e9ded4e91f0dbbf922aa38e8caf878665 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 11:11:24 -0700 Subject: [PATCH 10/11] Further block resolution of primitive types --- VERSION.txt | 5 +-- .../com/fasterxml/classmate/TypeBindings.java | 20 +++++++++++- .../com/fasterxml/classmate/TypeResolver.java | 9 +----- .../classmate/TestSubtypeResolution.java | 31 +++++++++++++++++++ .../fasterxml/classmate/TypeBindingsTest.java | 13 ++++++++ 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index fed6dd0..e67d285 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -32,8 +32,9 @@ Release notes: longer included in types returned by `resolveSubtype()`: raw self-references resolve to their bounds, others to the referenced type (self-references to enclosing type, like in raw `Enum`, are retained as-is) -- `TypeResolver.resolve(Type, Type...)` now fails with `IllegalArgumentException` - if any of type parameters is a primitive type (like `resolve(List.class, int.class)`) +- `TypeResolver.resolve(Type, Type...)` and `TypeBindings.create()` now fail with + `IllegalArgumentException` if any of type parameters is a primitive type + (like `resolve(List.class, int.class)`) (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/TypeBindings.java b/src/main/java/com/fasterxml/classmate/TypeBindings.java index cf229d8..5e36d07 100644 --- a/src/main/java/com/fasterxml/classmate/TypeBindings.java +++ b/src/main/java/com/fasterxml/classmate/TypeBindings.java @@ -86,6 +86,9 @@ public static TypeBindings emptyBindings() { /** * Factory method for constructing bindings for given class using specified type * parameters. + * + * @throws IllegalArgumentException If number of type parameters does not match + * that of given class, or if any of type parameters is a primitive type */ public static TypeBindings create(Class erasedType, List typeList) { @@ -93,7 +96,14 @@ public static TypeBindings create(Class erasedType, List typeLi NO_TYPES : typeList.toArray(new ResolvedType[0]); return create(erasedType, types); } - + + /** + * Factory method for constructing bindings for given class using specified type + * parameters. + * + * @throws IllegalArgumentException If number of type parameters does not match + * that of given class, or if any of type parameters is a primitive type + */ public static TypeBindings create(Class erasedType, ResolvedType[] types) { if (types == null) { @@ -116,6 +126,14 @@ public static TypeBindings create(Class erasedType, ResolvedType[] types) +" with "+types.length+" type parameter" +((types.length == 1) ? "" : "s")+": class expects "+names.length); } + // [classmate#127]: primitive types are not valid type parameters + for (int i = 0, len = types.length; i < len; ++i) { + if (types[i].isPrimitive()) { + throw new IllegalArgumentException("Can not use primitive type (" + +types[i].getBriefDescription()+") as type parameter #" + +(i+1)+"/"+len+" of "+erasedType.getName()); + } + } return new TypeBindings(names, types, null); } diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 0de59fe..a4bd60c 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -165,14 +165,7 @@ public ResolvedType resolve(Type type, Type... typeParameters) int len = typeParameters.length; ResolvedType[] resolvedParams = new ResolvedType[len]; for (int i = 0; i < len; ++i) { - final ResolvedType param = _fromAny(null, typeParameters[i], bindings); - // [classmate#127]: primitive types are not valid type parameters - if (param.isPrimitive()) { - throw new IllegalArgumentException("Can not use primitive type (" - +param.getBriefDescription()+") as type parameter #" - +(i+1)+"/"+len+" of "+rawBase.getName()); - } - resolvedParams[i] = param; + resolvedParams[i] = _fromAny(null, typeParameters[i], bindings); } return _fromClass(null, rawBase, TypeBindings.create(rawBase, resolvedParams)); } diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index fb788d7..6e89367 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -86,6 +86,12 @@ static class KVPair extends Pair { } static class EnumHolder> extends Wrapper> { } + @SuppressWarnings("rawtypes") + static class Node { } + + @SuppressWarnings("rawtypes") + static class NodeOfRawNodeWrapper extends Wrapper> { } + abstract static class OuterType extends AbstractMap> { public abstract class Inner extends AbstractMap> { @@ -235,6 +241,17 @@ public void testSubtypeOfSelfReference() assertEquals(rawEnum, typeResolver.resolveSubtype(supertype, Enum.class)); } + // [classmate#127]: self-reference (in raw `Node`) compared to non-self-reference + // type (`Node`) in subtype + public void testSubtypeWithSelfReferenceComparedToType() + { + ResolvedType supertype = typeResolver.resolve(Wrapper.class, Node.class); + assertTrue(TypeResolver.isSelfReference( + supertype.getTypeParameters().get(0).getTypeParameters().get(0))); + assertSame(NodeOfRawNodeWrapper.class, + typeResolver.resolveSubtype(supertype, NodeOfRawNodeWrapper.class).getErasedType()); + } + // [classmate#127]: self-reference to another instance of the same class (from // different resolution context) must not be retained public void testSubtypeWithSelfReferenceFromOtherContext() @@ -357,6 +374,20 @@ public void testSubtypeWithNestedWildcardAndRepeatedTypeVariable() supertype = typeResolver.resolve(new GenericType, Map>>() { }); assertEquals(typeResolver.resolve(Map.class, String.class, Integer.class), typeResolver.resolveSubtype(supertype, SamePair.class).getTypeParameters().get(0)); + + // more specific binding first + supertype = typeResolver.resolve(new GenericType, List>>() { }); + assertEquals(listOfString, + typeResolver.resolveSubtype(supertype, SamePair.class).getTypeParameters().get(0)); + supertype = typeResolver.resolve(Pair.class, String[].class, Object[].class); + assertEquals(typeResolver.resolve(String[].class), + typeResolver.resolveSubtype(supertype, SamePair.class).getTypeParameters().get(0)); + + // and arrays with element type merged from both + supertype = typeResolver.resolve(new GenericType[], Map[]>>() { }); + ResolvedType merged = typeResolver.resolveSubtype(supertype, SamePair.class).getTypeParameters().get(0); + assertEquals(typeResolver.resolve(new GenericType[]>() { }), merged); + assertSame(Map[].class, merged.getErasedType()); } // [classmate#127]: bounds of type variables are not verified (wildcard upper bound diff --git a/src/test/java/com/fasterxml/classmate/TypeBindingsTest.java b/src/test/java/com/fasterxml/classmate/TypeBindingsTest.java index 537f267..e602d37 100644 --- a/src/test/java/com/fasterxml/classmate/TypeBindingsTest.java +++ b/src/test/java/com/fasterxml/classmate/TypeBindingsTest.java @@ -23,6 +23,19 @@ public void create() throws NoSuchFieldException, IllegalAccessException { assertEquals(0, ((ResolvedType[]) typesField.get(instance)).length); } + // [classmate#127]: primitive types are not valid type parameters + @Test + public void createWithPrimitive() { + ResolvedType intType = new TypeResolver().resolve(Integer.TYPE); + try { + TypeBindings.create(Comparable.class, Collections.singletonList(intType)); + fail("Expected failure"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage(), e.getMessage().contains( + "Can not use primitive type (int) as type parameter #1/1 of java.lang.Comparable")); + } + } + @Test public void isEmpty() { TypeBindings instance = TypeBindings.create(String.class, (List) null); From 6f73c133801a8ac611ccfa45bdb11896ff3e6318 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 11:17:52 -0700 Subject: [PATCH 11/11] Test adds --- .../classmate/TestSubtypeResolution.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index 6e89367..fb84f3f 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -1,5 +1,7 @@ package com.fasterxml.classmate; +import com.fasterxml.classmate.types.ResolvedObjectType; +import com.fasterxml.classmate.types.ResolvedRecursiveType; import com.fasterxml.classmate.util.ResolvedTypeCache; import java.util.*; @@ -738,4 +740,48 @@ public void testObjectArrayNotCompatibleWithPrimitiveArray() { _verifyIncompatible(typeResolver.resolve(Wrapper.class, Object[].class), IntArrayWrapper.class); } + + // [classmate#127]: self-reference (in raw type) is not merged with other type + // (raw types not yet handled as such, see [classmate#130]) + public void testSelfReferenceNotMergedWithOtherType() + { + ResolvedType rawEnum = typeResolver.resolve(Enum.class); + ResolvedType enumOfTimeUnit = typeResolver.resolve(Enum.class, java.util.concurrent.TimeUnit.class); + _verifyConflict(typeResolver.resolve(Pair.class, rawEnum, enumOfTimeUnit), SamePair.class); + _verifyConflict(typeResolver.resolve(Pair.class, enumOfTimeUnit, rawEnum), SamePair.class); + _verifyConflict(typeResolver.resolve(Pair.class, + typeResolver.resolve(Node.class), typeResolver.resolve(Node.class, Node.class)), + SamePair.class); + } + + // [classmate#127]: types constructed directly (not via `TypeResolver`) may lack + // bindings for type parameters + public void testSubtypeWithTypeMissingBindings() + { + ResolvedType rawList = new ResolvedObjectType(List.class, TypeBindings.emptyBindings(), + (ResolvedType) null, (List) null); + _verifyIncompatible(typeResolver.resolve(Wrapper.class, rawList), ListWrapper.class); + _verifyConflict(typeResolver.resolve(Pair.class, rawList, + typeResolver.resolve(List.class, String.class)), SamePair.class); + } + + // [classmate#127]: self-reference constructed directly (not via `TypeResolver`) + // may lack referenced type + public void testSubtypeWithUnresolvedSelfReference() + { + ResolvedType listOfString = typeResolver.resolve(List.class, String.class); + ResolvedType selfRef = new ResolvedRecursiveType(List.class, listOfString.getTypeBindings()); + assertEquals(typeResolver.resolve(ListWrapper.class, String.class), + typeResolver.resolveSubtype(typeResolver.resolve(Wrapper.class, selfRef), ListWrapper.class)); + } + + private void _verifyConflict(ResolvedType supertype, Class subtype) + { + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, subtype); + fail("Expected failure, got: "+t.getFullDescription()); + } catch (IllegalArgumentException e) { + verifyException(e, "conflicting bindings for type variable `E`"); + } + } }