diff --git a/VERSION.txt b/VERSION.txt index 099f511..e67d285 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -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 extends Pair`; 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`) 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) 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 b1055fa..a4bd60c 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) { @@ -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 @@ -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; @@ -596,8 +598,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) + private void _resolveTypePlaceholders(ResolvedType sourceType, ResolvedType actualType, + Class subtype, TypePlaceHolder[] placeholders) throws IllegalArgumentException { List expectedTypes = sourceType.getTypeParameters(); @@ -605,20 +610,71 @@ 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)) { - 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()) { @@ -627,13 +683,201 @@ 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()) { + 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 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; + } + + /** + * 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>}) + * 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 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; + } + } } diff --git a/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java b/src/test/java/com/fasterxml/classmate/TestSubtypeResolution.java index 8a44f5c..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.*; @@ -31,6 +33,67 @@ 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 { } + + 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 { } + + static class RawSelfOther extends Wrapper { } + + static class SubWrapper extends Wrapper { } + + static class SelfInList extends Wrapper> { } + + static class SamePair extends Pair { } + + static class RecInList extends Wrapper>> { } + + static class EnumPair> extends Pair, E> { } + + static class IntOnlyWrapper extends Wrapper { } + + static class Builder> { } + + 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 { } + + 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> { @@ -75,6 +138,291 @@ 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()); + } + + // [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(TypeResolver.isSelfReference(supertype.getTypeParameters().get(0).getArrayElementType())); + ResolvedType subtype = typeResolver.resolveSubtype(supertype, ArrayWrapper.class); + ResolvedType param = subtype.getTypeParameters().get(0); + assertFalse(TypeResolver.isSelfReference(param)); + assertSame(RawSelfArray.class, param.getErasedType()); + assertNotNull(param.getParentClass()); + 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 to referenced type + public void testSubtypeOfSelfReference() + { + ResolvedType rawEnum = typeResolver.resolve(Enum.class); + ResolvedType supertype = rawEnum.getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(supertype)); + 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() + { + 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 + 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); + } + + // [classmate#127]: `Object` type parameters (possibly from wildcard or raw type) + // are not verified against bounds + public void testSubtypeWithObjectNotVerifiedAgainstBounds() + { + // `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(ComparableNumWrapper.class, Object.class), + typeResolver.resolveSubtype(wrapper, ComparableNumWrapper.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)); + assertEquals(typeResolver.resolve(Enum.class), param); + assertNotNull(param.getParentClass()); + } + + // [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)); + + // 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 + // 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), + 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)); + 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. @@ -308,4 +656,132 @@ 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"); + } + } + + // [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() + { + 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, "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) + { + try { + ResolvedType t = typeResolver.resolveSubtype(supertype, subtype); + fail("Expected failure, got: "+t.getFullDescription()); + } catch (IllegalArgumentException e) { + 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); + } + + // [classmate#127]: `Object[]` is not compatible with primitive array + 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`"); + } + } } 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); 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);