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
15 changes: 15 additions & 0 deletions VERSION.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ 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
- `resolveSubtype()` now rejects type variable bound to conflicting types
(like `E` in `Dup<E> extends Pair<E[], E>`; except that `Object`, which may
come from wildcard or raw type, is compatible with more specific type at any
level of nesting, like `List<?>` with `List<String>`) 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, others to the referenced type (self-references to
enclosing type, like in raw `Enum`, are retained as-is)
- `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)

Expand Down
20 changes: 19 additions & 1 deletion src/main/java/com/fasterxml/classmate/TypeBindings.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,24 @@ 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<ResolvedType> typeList)
{
ResolvedType[] types = (typeList == null || typeList.isEmpty()) ?
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) {
Expand All @@ -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);
}

Expand Down
274 changes: 259 additions & 15 deletions src/main/java/com/fasterxml/classmate/TypeResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -235,11 +238,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: [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
Expand Down Expand Up @@ -281,7 +283,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, placeholders);
// And then re-construct, if necessary
if (paramCount == 0) { // if no type parameters, fine as is
return resolvedSubtype;
Expand Down Expand Up @@ -596,29 +598,83 @@ 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)
private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actualType,
Class<?> subtype, TypePlaceHolder[] placeholders)
throws IllegalArgumentException
{
List<ResolvedType> expectedTypes = sourceType.getTypeParameters();
List<ResolvedType> actualTypes = actualType.getTypeParameters();
for (int i = 0, len = expectedTypes.size(); i < len; ++i) {
ResolvedType exp = expectedTypes.get(i);
ResolvedType act = actualTypes.get(i);
if (!_verifyAndResolve(exp, act)) {
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);
}
}

/**
* @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)
{
// 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;
}
// [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 compatible type
ResolvedType merged = _mergeBindings(prev, exp);
if (merged == null) {
throw new BindingConflict(placeholder, exp);
}
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);
}
// [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()) {
return false;
}
if (exp.isArray()) {
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()) {
Expand All @@ -627,13 +683,201 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act)
// But we can check type parameters "blindly"
List<ResolvedType> expectedTypes = exp.getTypeParameters();
List<ResolvedType> 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()) {
return false;
}
for (int i = 0; i < len; ++i) {
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<ResolvedType> paramsA = a.getTypeParameters();
final List<ResolvedType> 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;
}

/**
* 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 (isSelfReference(type)
&& type.getTypeBindings().isEmpty()
&& type.getErasedType().getTypeParameters().length > 0) {
return _fromClass(null, type.getErasedType(), type.getTypeBindings());
}
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
* 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<E extends Enum<E>>})
* which are valid as-is.
*
* @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<ResolvedType> enclosing)
{
if (!TypeBindings.isContextual(type)) {
return type;
}
if (type.isArray()) {
ResolvedType elem = type.getArrayElementType();
ResolvedType newElem = _resolveSelfReferences(elem, enclosing);
return (newElem == elem) ? type : _arrayOf(type.getErasedType(), newElem);
}
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);
}
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) {
ResolvedType t = bindings.getBoundType(i);
ResolvedType newT = _resolveSelfReferences(t, enclosing);
if (newT != t) {
if (newTypes == null) {
newTypes = bindings.getTypeParameters().toArray(new ResolvedType[0]);
}
newTypes[i] = newT;
}
}
enclosing.remove(enclosing.size() - 1);
if (newTypes == null) {
return type;
}
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;
}
}
}
Loading
Loading