From eca450cbc30d85e7f0b68d435f7c00747e30af22 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 20:58:43 -0700 Subject: [PATCH] Fix #130: `resolveSubtype()` treats wildcard and raw type parameters as exact types instead of upper bounds --- VERSION.txt | 6 ++ .../com/fasterxml/classmate/TypeResolver.java | 73 +++++++++++++++++++ .../classmate/TestSubtypeResolution.java | 56 +++++++++++++- 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index aaf6197..6713bee 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -51,6 +51,12 @@ Release notes: bindings: see new `ResolvedRecursiveType.getActualType()` (also used for `getParentClass()`, members and `resolveSubtype()`) (fix by @cowtowncoder w/ Claude code) +#130: `resolveSubtype()` does not verify bounds of type variables +- `resolveSubtype()` now rejects type variable bound to type that can not satisfy + its bounds (like `String` for `E extends Number`); check is lenient since bindings + from wildcards and raw types are not distinguished from exact ones: only types that + can not have a common subtype with bound (as per Java casting rules) are rejected + (fix by @cowtowncoder w/ Claude code) #132: `MemberResolver` misses members inherited via self-referential interface (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 a41e1a0..7d210f6 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -307,6 +307,8 @@ public ResolvedType resolveSubtype(ResolvedType supertype, final Class subtyp } typeParams[i] = t; } + // [classmate#130]: verify that bindings can satisfy bounds of type variables + _verifyBounds(subtype, typeParams); return resolve(subtype, typeParams); } @@ -791,6 +793,77 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) return true; } + /** + * Helper method for verifying that types bound to type variables of given subtype + * could satisfy bounds of those variables. + *

+ * NOTE: since bindings derived from wildcards (and raw types) are resolved to their + * upper bounds (like {@code Number} for {@code ? extends Number}), they can not be + * told apart from exact bindings: so check is lenient, only failing if binding and + * bound can not have a common subtype (as per Java casting rules, using erased types). + * + * @throws IllegalArgumentException If a binding can not satisfy bound of its type variable + * + * @since 1.8 + */ + private static void _verifyBounds(Class subtype, ResolvedType[] typeParams) + { + final TypeVariable[] vars = subtype.getTypeParameters(); + for (int i = 0; i < vars.length; ++i) { + final Class boundType = typeParams[i].getErasedType(); + for (Type b : vars[i].getBounds()) { + Class erasedBound = _erasedBound(b, vars, typeParams); + if ((erasedBound != null) && _areDisjoint(boundType, erasedBound)) { + throw new IllegalArgumentException(String.format( + "Type parameter #%d/%d (`%s`) of %s bound to %s, not within its bound (%s)", + i+1, vars.length, vars[i].getName(), subtype.getName(), + typeParams[i].getBriefDescription(), b.getTypeName())); + } + } + } + } + + /** + * @return Erased type of given bound; {@code null} if not known (like for type + * variable not declared by the type itself) + */ + 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) { + int ix = Arrays.asList(vars).indexOf(bound); + if (ix >= 0) { + return typeParams[ix].getErasedType(); + } + } + return null; + } + + /** + * Helper method for checking whether given (erased) types can not have a common + * subtype: that is, neither is subtype of the other, and either one is final + * (or an array type), or neither is an interface. + */ + private static boolean _areDisjoint(Class a, Class b) + { + if (a.isAssignableFrom(b) || b.isAssignableFrom(a)) { + return false; + } + if (_isFinal(a) || _isFinal(b)) { + return true; + } + return !a.isInterface() && !b.isInterface(); + } + + private static boolean _isFinal(Class type) { + return type.isArray() || type.isPrimitive() || Modifier.isFinal(type.getModifiers()); + } + /** * 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 diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index fb84f3f..aa00d15 100644 --- a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java +++ b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java @@ -88,6 +88,10 @@ static class KVPair extends Pair { } static class EnumHolder> extends Wrapper> { } + static class SelfParam extends Wrapper> { } + + static class SelfParamSub extends SelfParam { } + @SuppressWarnings("rawtypes") static class Node { } @@ -392,8 +396,8 @@ public void testSubtypeWithNestedWildcardAndRepeatedTypeVariable() assertSame(Map[].class, merged.getErasedType()); } - // [classmate#127]: bounds of type variables are not verified (wildcard upper bound - // can not be distinguished from actual type; see [classmate#130]) + // [classmate#127]: bounds of type variables are only verified leniently (wildcard + // upper bound can not be distinguished from actual type; see [classmate#130]) public void testSubtypeOfBoundedWildcard() { ResolvedType supertype = typeResolver.resolve(new GenericType>() { }); @@ -415,6 +419,54 @@ public void testSubtypeSatisfyingBounds() BuilderWrapper.class)); } + // [classmate#130]: bindings that can not satisfy bounds of type variables + public void testSubtypeViolatingBounds() + { + _verifyOutOfBounds(typeResolver.resolve(Wrapper.class, String[].class), NumArrayWrapper.class); + _verifyOutOfBounds(typeResolver.resolve(Wrapper.class, String.class), IntOnlyWrapper.class); + _verifyOutOfBounds(typeResolver.resolve(Wrapper.class, Long.class), IntOnlyWrapper.class); + _verifyOutOfBounds(typeResolver.resolve(Wrapper.class, String.class), ComparableNumWrapper.class); + _verifyOutOfBounds(typeResolver.resolve(Wrapper.class, String.class), BuilderWrapper.class); + // bound to another type variable + _verifyOutOfBounds(typeResolver.resolve(Pair.class, Integer.class, String.class), KVPair.class); + } + + // [classmate#130]: bindings that could satisfy bounds (as bindings from wildcards + // and raw types may represent subtypes of bound types) are accepted + public void testSubtypePossiblySatisfyingBounds() + { + // `Object` (like from raw type) may be anything + assertEquals(typeResolver.resolve(IntOnlyWrapper.class, Object.class), + typeResolver.resolveSubtype(typeResolver.resolve(Wrapper.class), IntOnlyWrapper.class)); + // as may non-final class and interface + assertEquals(typeResolver.resolve(ComparableNumWrapper.class, Runnable.class), + typeResolver.resolveSubtype(typeResolver.resolve(Wrapper.class, Runnable.class), + ComparableNumWrapper.class)); + assertEquals(typeResolver.resolve(KVPair.class, Number.class, Integer.class), + typeResolver.resolveSubtype(typeResolver.resolve(Pair.class, Number.class, Integer.class), + KVPair.class)); + } + + // [classmate#130]: self-reference with bindings different from referenced type + // resolves to type with its own bindings + public void testSubtypeOfSelfReferenceWithOwnBindings() + { + ResolvedType selfRef = typeResolver.resolve(SelfParam.class, Integer.class) + .getParentClass().getTypeParameters().get(0); + assertEquals(typeResolver.resolve(SelfParamSub.class, String.class), + typeResolver.resolveSubtype(selfRef, SelfParamSub.class)); + } + + private void _verifyOutOfBounds(ResolvedType supertype, Class subtype) + { + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, subtype); + fail("Expected failure, got: "+t.getFullDescription()); + } catch (IllegalArgumentException e) { + verifyException(e, "not within its bound"); + } + } + private void _verifyNoSelfReference(ResolvedType type, Class expRaw) { assertFalse(TypeResolver.isSelfReference(type));