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