Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions VERSION.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
73 changes: 73 additions & 0 deletions src/main/java/com/fasterxml/classmate/TypeResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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.
*<p>
* 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
Expand Down
56 changes: 54 additions & 2 deletions src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ static class KVPair<K, V extends K> extends Pair<K, V> { }

static class EnumHolder<E extends Enum<E>> extends Wrapper<Enum<E>> { }

static class SelfParam<T> extends Wrapper<SelfParam<String>> { }

static class SelfParamSub<X> extends SelfParam<X> { }

@SuppressWarnings("rawtypes")
static class Node<N extends Node> { }

Expand Down Expand Up @@ -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<Wrapper<? extends Number>>() { });
Expand All @@ -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));
Expand Down
Loading