This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new 99618eb84 Fix TypeUtils.isAssignable rejecting Class<X> to Class<?>
assignments (#1820)
99618eb84 is described below
commit 99618eb840cf18d31413c4fdfad2e4503322fd70
Author: XenoAmess <[email protected]>
AuthorDate: Fri Oct 16 03:28:48 2026 +0800
Fix TypeUtils.isAssignable rejecting Class<X> to Class<?> assignments
(#1820)
* Fix TypeUtils.isAssignable() rejecting Class<X> to Class<?> assignments
(#1548 follow-up)
The Class-specialized check added for #1548 (LANG-1749) requires the type
argument of java.lang.Class to match exactly, but it did not account for
wildcard type arguments on the target side. As a result, assignments that
the Java compiler accepts are rejected:
Class<Integer> -> Class<?> javac: OK, TypeUtils: false
Class<String> -> Class<? extends Object> javac: OK, TypeUtils: false
Class<List<String>> -> Class<? extends Collection<?>>: false
When the target type argument is a wildcard, check that the subject type
argument is contained by the wildcard instead of demanding equality. A raw
generic class still does not satisfy a parameterized bound, since Class is
invariant and javac does not apply unchecked conversion to its type
argument, keeping the #1548 cases (e.g. Class<TestImpl> to
Class<? extends TestIF<?>>) rejected.
* Handle source wildcards and type variables in Class assignability
Reuse wildcard containment checks to support upper and lower bounds, and
preserve source type variables declared outside java.lang.Class. Limit the raw
generic argument guard to parameterized upper bounds so legal lower-bound
assignments remain accepted.
Add regression tests for these cases. All five new test methods fail with
the original upstream Class check; the default mvn build passes with the fix.
* [LANG-1749] Reject unchecked wildcard bounds in Class assignability
Propagate a no-unchecked-conversion mode through wildcard bounds, generic
array components, and parameterized supertypes. Reject raw ancestors and
preserve external type variables while resolving parameterized parents.
This applies the same subtype checks to upper and lower wildcard bounds.
Return false when unrelated source type variables have no matching bound
instead of throwing IllegalStateException.
Add 39 compiler-checked Class wildcard assignment cases and a direct type
variable regression test. The original PR implementation produces 16
assertion failures and 6 errors with the new tests.
---
.../apache/commons/lang3/reflect/TypeUtils.java | 143 +++++++++++++---
.../commons/lang3/reflect/TypeUtilsTest.java | 187 +++++++++++++++++++++
2 files changed, 307 insertions(+), 23 deletions(-)
diff --git a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
index 82b80f1b4..f56db9e4e 100644
--- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
+++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
@@ -19,6 +19,7 @@
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
+import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
@@ -1030,9 +1031,11 @@ private static boolean isAssignable(final Type type,
final Class<?> toClass) {
* @param type The subject type to be assigned to the target
type.
* @param toGenericArrayType The target generic array type.
* @param typeVarAssigns A map with type variables.
+ * @param allowUnchecked Whether unchecked conversions are allowed.
* @return {@code true} if {@code type} is assignable to {@code
toGenericArrayType}.
*/
- private static boolean isAssignable(final Type type, final
GenericArrayType toGenericArrayType, final Map<TypeVariable<?>, Type>
typeVarAssigns) {
+ private static boolean isAssignable(final Type type, final
GenericArrayType toGenericArrayType, final Map<TypeVariable<?>, Type>
typeVarAssigns,
+ final boolean allowUnchecked) {
if (type == null) {
return true;
}
@@ -1049,16 +1052,16 @@ private static boolean isAssignable(final Type type,
final GenericArrayType toGe
if (type instanceof Class<?>) {
final Class<?> cls = (Class<?>) type;
// compare the component types
- return cls.isArray() && isAssignable(cls.getComponentType(),
toComponentType, typeVarAssigns);
+ return cls.isArray() && isAssignable(cls.getComponentType(),
toComponentType, typeVarAssigns, allowUnchecked);
}
if (type instanceof GenericArrayType) {
// compare the component types
- return isAssignable(((GenericArrayType)
type).getGenericComponentType(), toComponentType, typeVarAssigns);
+ return isAssignable(((GenericArrayType)
type).getGenericComponentType(), toComponentType, typeVarAssigns,
allowUnchecked);
}
if (type instanceof WildcardType) {
// so long as one of the upper bounds is assignable, it's good
for (final Type bound : getImplicitUpperBounds((WildcardType)
type)) {
- if (isAssignable(bound, toGenericArrayType)) {
+ if (isAssignable(bound, toGenericArrayType, typeVarAssigns,
allowUnchecked)) {
return true;
}
}
@@ -1068,7 +1071,7 @@ private static boolean isAssignable(final Type type,
final GenericArrayType toGe
// probably should remove the following logic and just return
false.
// type variables cannot specify arrays as bounds.
for (final Type bound : getImplicitBounds((TypeVariable<?>) type))
{
- if (isAssignable(bound, toGenericArrayType)) {
+ if (isAssignable(bound, toGenericArrayType, typeVarAssigns,
allowUnchecked)) {
return true;
}
}
@@ -1089,9 +1092,11 @@ private static boolean isAssignable(final Type type,
final GenericArrayType toGe
* @param type The subject type to be assigned to the
target type.
* @param toParameterizedType The target parameterized type.
* @param typeVarAssigns A map with type variables.
+ * @param allowUnchecked Whether unchecked conversions are allowed.
* @return {@code true} if {@code type} is assignable to {@code toType}.
*/
- private static boolean isAssignable(final Type type, final
ParameterizedType toParameterizedType, final Map<TypeVariable<?>, Type>
typeVarAssigns) {
+ private static boolean isAssignable(final Type type, final
ParameterizedType toParameterizedType, final Map<TypeVariable<?>, Type>
typeVarAssigns,
+ final boolean allowUnchecked) {
if (type == null) {
return true;
}
@@ -1110,6 +1115,43 @@ private static boolean isAssignable(final Type type,
final ParameterizedType toP
}
// get the target type's raw type
final Class<?> toClass = getRawType(toParameterizedType);
+ if (!allowUnchecked) {
+ // Type argument bounds require subtyping, so a raw supertype
cannot supply missing type arguments.
+ if (type instanceof TypeVariable<?>) {
+ for (final Type bound : getImplicitBounds((TypeVariable<?>)
type)) {
+ if (isAssignable(bound, toParameterizedType,
typeVarAssigns, false)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (type instanceof Class<?>) {
+ final Class<?> fromClass = (Class<?>) type;
+ if (isRawGenericClass(fromClass) || !isAssignable(fromClass,
toClass)) {
+ return false;
+ }
+ return toClass.equals(fromClass)
+ || isAssignable(getClosestParentType(fromClass,
toClass), toParameterizedType, typeVarAssigns, false);
+ }
+ if (type instanceof ParameterizedType) {
+ final ParameterizedType fromType = (ParameterizedType) type;
+ final Class<?> fromClass = getRawType(fromType);
+ if (!toClass.equals(fromClass)) {
+ if (!isAssignable(fromClass, toClass)) {
+ return false;
+ }
+ final Map<TypeVariable<?>, Type> fromAssignments =
getTypeArguments(fromType);
+ // Preserve externally declared variables when
substituting the parent type.
+ for (final Type argument : new
HashMap<>(fromAssignments).values()) {
+ if (argument instanceof TypeVariable<?>) {
+ fromAssignments.putIfAbsent((TypeVariable<?>)
argument, argument);
+ }
+ }
+ final Type parent = unrollVariables(fromAssignments,
getClosestParentType(fromClass, toClass));
+ return isAssignable(parent, toParameterizedType,
typeVarAssigns, false);
+ }
+ }
+ }
// get the subject type's type arguments including owner type arguments
// and supertype arguments up to and including the target class.
final Map<TypeVariable<?>, Type> fromTypeVarAssigns =
getTypeArguments(type, toClass, null);
@@ -1118,10 +1160,10 @@ private static boolean isAssignable(final Type type,
final ParameterizedType toP
return false;
}
// compatible types, but there's no type arguments. this is equivalent
- // to comparing Map< ?, ? > to Map, and raw types are always assignable
- // to parameterized types.
+ // to comparing Map< ?, ? > to Map. Raw types are assignable to
+ // parameterized types only when unchecked conversion is allowed.
if (fromTypeVarAssigns.isEmpty()) {
- return true;
+ return allowUnchecked;
}
// get the target type's type arguments including owner type arguments
final Map<TypeVariable<?>, Type> toTypeVarAssigns =
getTypeArguments(toParameterizedType, toClass, typeVarAssigns);
@@ -1130,16 +1172,24 @@ private static boolean isAssignable(final Type type,
final ParameterizedType toP
final TypeVariable<?>[] typeParams = toClass.getTypeParameters();
if (typeParams.length > 0) {
final Type toTypeArg =
unrollVariableAssignments(typeParams[0], toTypeVarAssigns);
- final Type fromTypeArg =
unrollVariableAssignments(typeParams[0], fromTypeVarAssigns);
- if (toTypeArg != null && (fromTypeArg == null ||
!toTypeArg.equals(fromTypeArg))) {
+ final Type fromTypeArg =
ObjectUtils.defaultIfNull(unrollVariableAssignments(typeParams[0],
fromTypeVarAssigns),
+ fromTypeVarAssigns.get(typeParams[0]));
+ if (toTypeArg != null && !toTypeArg.equals(fromTypeArg) &&
!(toTypeArg instanceof WildcardType
+ && isAssignableToClassWildcard(fromTypeArg,
(WildcardType) toTypeArg, typeVarAssigns))) {
return false;
}
}
}
// now to check each type argument
- for (final TypeVariable<?> var : toTypeVarAssigns.keySet()) {
- final Type toTypeArg = unrollVariableAssignments(var,
toTypeVarAssigns);
- final Type fromTypeArg = unrollVariableAssignments(var,
fromTypeVarAssigns);
+ for (final Map.Entry<TypeVariable<?>, Type> assignment :
toTypeVarAssigns.entrySet()) {
+ final TypeVariable<?> var = assignment.getKey();
+ final Type toTypeArg = allowUnchecked ?
unrollVariableAssignments(var, toTypeVarAssigns)
+ : ObjectUtils.defaultIfNull(unrollVariableAssignments(var,
toTypeVarAssigns), assignment.getValue());
+ final Type fromTypeArg = allowUnchecked ?
unrollVariableAssignments(var, fromTypeVarAssigns)
+ : ObjectUtils.defaultIfNull(unrollVariableAssignments(var,
fromTypeVarAssigns), fromTypeVarAssigns.get(var));
+ if (!allowUnchecked && fromTypeArg == null && toTypeArg != null) {
+ return false;
+ }
if (toTypeArg == null && fromTypeArg instanceof Class) {
continue;
}
@@ -1147,13 +1197,43 @@ private static boolean isAssignable(final Type type,
final ParameterizedType toP
// the bounds of the wildcard type, or be an exact match to the
// parameters of the target type.
if (fromTypeArg != null && toTypeArg != null &&
!toTypeArg.equals(fromTypeArg)
- && !(toTypeArg instanceof WildcardType &&
isAssignable(fromTypeArg, toTypeArg, typeVarAssigns))) {
+ && !(toTypeArg instanceof WildcardType &&
isAssignable(fromTypeArg, toTypeArg, typeVarAssigns, allowUnchecked))) {
return false;
}
}
return true;
}
+ /**
+ * Tests whether the type argument {@code fromTypeArg} of a {@code Class}
parameterized type satisfies the wildcard type
+ * argument of a target {@code Class} parameterized type. Unchecked
conversion does not apply inside a type argument,
+ * including when checking the upper or lower bounds of a wildcard.
+ *
+ * @param fromTypeArg the subject type argument.
+ * @param wildcardType the target wildcard type argument.
+ * @param typeVarAssigns a map with type variables.
+ * @return {@code true} if {@code fromTypeArg} is contained by {@code
wildcardType}.
+ */
+ private static boolean isAssignableToClassWildcard(final Type fromTypeArg,
final WildcardType wildcardType,
+ final Map<TypeVariable<?>, Type> typeVarAssigns) {
+ return fromTypeArg != null && isAssignable(fromTypeArg, wildcardType,
typeVarAssigns, false);
+ }
+
+ /**
+ * Tests whether {@code type} is a raw generic class or a non-static
member of a raw generic class.
+ *
+ * @param type the type to test.
+ * @return {@code true} if {@code type} is a raw generic class.
+ */
+ private static boolean isRawGenericClass(final Type type) {
+ if (!(type instanceof Class<?>)) {
+ return false;
+ }
+ final Class<?> cls = (Class<?>) type;
+ return cls.getTypeParameters().length > 0
+ || !Modifier.isStatic(cls.getModifiers()) &&
isRawGenericClass(cls.getDeclaringClass());
+ }
+
/**
* Tests if the subject type may be implicitly cast to the target type
following the Java generics rules. If both types are {@link Class} objects, the
* method returns the result of {@link ClassUtils#isAssignable(Class,
Class)}.
@@ -1175,17 +1255,31 @@ public static boolean isAssignable(final Type type,
final Type toType) {
* @return {@code true} if {@code type} is assignable to {@code toType}.
*/
private static boolean isAssignable(final Type type, final Type toType,
final Map<TypeVariable<?>, Type> typeVarAssigns) {
+ return isAssignable(type, toType, typeVarAssigns, true);
+ }
+
+ /**
+ * Tests assignability, optionally allowing unchecked conversions from raw
types to parameterized types.
+ *
+ * @param type The subject type.
+ * @param toType The target type.
+ * @param typeVarAssigns Optional type variable assignments.
+ * @param allowUnchecked Whether unchecked conversions are allowed.
+ * @return whether the subject type is assignable to the target type.
+ */
+ private static boolean isAssignable(final Type type, final Type toType,
final Map<TypeVariable<?>, Type> typeVarAssigns,
+ final boolean allowUnchecked) {
if (toType == null || toType instanceof Class<?>) {
return isAssignable(type, (Class<?>) toType);
}
if (toType instanceof ParameterizedType) {
- return isAssignable(type, (ParameterizedType) toType,
typeVarAssigns);
+ return isAssignable(type, (ParameterizedType) toType,
typeVarAssigns, allowUnchecked);
}
if (toType instanceof GenericArrayType) {
- return isAssignable(type, (GenericArrayType) toType,
typeVarAssigns);
+ return isAssignable(type, (GenericArrayType) toType,
typeVarAssigns, allowUnchecked);
}
if (toType instanceof WildcardType) {
- return isAssignable(type, (WildcardType) toType, typeVarAssigns);
+ return isAssignable(type, (WildcardType) toType, typeVarAssigns,
allowUnchecked);
}
if (toType instanceof TypeVariable<?>) {
return isAssignable(type, (TypeVariable<?>) toType,
typeVarAssigns);
@@ -1224,6 +1318,7 @@ private static boolean isAssignable(final Type type,
final TypeVariable<?> toTyp
return true;
}
}
+ return false;
}
if (type instanceof Class<?> || type instanceof ParameterizedType ||
type instanceof GenericArrayType || type instanceof WildcardType) {
return false;
@@ -1237,9 +1332,11 @@ private static boolean isAssignable(final Type type,
final TypeVariable<?> toTyp
* @param type The subject type to be assigned to the target
type.
* @param toWildcardType The target wildcard type.
* @param typeVarAssigns A map with type variables.
+ * @param allowUnchecked Whether unchecked conversions are allowed.
* @return {@code true} if {@code type} is assignable to {@code
toWildcardType}.
*/
- private static boolean isAssignable(final Type type, final WildcardType
toWildcardType, final Map<TypeVariable<?>, Type> typeVarAssigns) {
+ private static boolean isAssignable(final Type type, final WildcardType
toWildcardType, final Map<TypeVariable<?>, Type> typeVarAssigns,
+ final boolean allowUnchecked) {
if (type == null) {
return true;
}
@@ -1266,7 +1363,7 @@ private static boolean isAssignable(final Type type,
final WildcardType toWildca
// each upper bound of the target type
boolean satisfied = false;
for (final Type bound : upperBounds) {
- if (isAssignable(bound, toBound, typeVarAssigns)) {
+ if (isAssignable(bound, toBound, typeVarAssigns,
allowUnchecked)) {
satisfied = true;
break;
}
@@ -1282,7 +1379,7 @@ private static boolean isAssignable(final Type type,
final WildcardType toWildca
// each lower bound of the target type has to be assignable to
// each lower bound of the subject type
for (final Type bound : lowerBounds) {
- if (!isAssignable(toBound, bound, typeVarAssigns)) {
+ if (!isAssignable(toBound, bound, typeVarAssigns,
allowUnchecked)) {
return false;
}
}
@@ -1292,14 +1389,14 @@ private static boolean isAssignable(final Type type,
final WildcardType toWildca
for (final Type toBound : toUpperBounds) {
// if there are assignments for unresolved type variables,
// now's the time to substitute them.
- if (!isAssignable(type, substituteTypeVariables(toBound,
typeVarAssigns), typeVarAssigns)) {
+ if (!isAssignable(type, substituteTypeVariables(toBound,
typeVarAssigns), typeVarAssigns, allowUnchecked)) {
return false;
}
}
for (final Type toBound : toLowerBounds) {
// if there are assignments for unresolved type variables,
// now's the time to substitute them.
- if (!isAssignable(substituteTypeVariables(toBound,
typeVarAssigns), type, typeVarAssigns)) {
+ if (!isAssignable(substituteTypeVariables(toBound,
typeVarAssigns), type, typeVarAssigns, allowUnchecked)) {
return false;
}
}
diff --git a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
index 73a2ed080..f68f0175d 100644
--- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
@@ -64,6 +64,7 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
/**
@@ -232,6 +233,49 @@ public static <U> Iterable<U>
methodWithGenericReturnType() {
}
}
+ private static class ClassWildcardTypes<T extends List, U, V extends
List<String>, W extends U> {
+ Class<List<String>> strings;
+ Class<List> rawList;
+ Class<ArrayList<String>> stringArrayList;
+ Class<? extends List> extendsRawList;
+ Class<? extends List<String>> extendsStrings;
+ Class<? extends List<?>> extendsUnknownList;
+ Class<? super List> superRawList;
+ Class<? super List<String>> superStrings;
+ Class<T> rawBound;
+ Class<? extends T> extendsRawBound;
+ Class<V> parameterizedBound;
+ Class<RawTestImpl> rawSubclass;
+ Class<ParameterizedRawTestImpl<String>> parameterizedRawSubclass;
+ Class<RawNumberTestImpl> rawAncestor;
+ Class<NumberTestImpl> fixedSubclass;
+ Class<GenericNumberOwner.Member> rawMember;
+ Class<GenericNumberOwner.StaticMember> staticMember;
+ Class<GenericNumberOwner<String>.Member> parameterizedMember;
+ Class<TestImpl<Number>> parameterizedSubclass;
+ Class<TestImpl<U>> variableSubclass;
+ Class<? extends TestIF<?>> extendsUnknownInterface;
+ Class<? extends TestIF<Number>> extendsNumberInterface;
+ Class<? extends TestIF<U>> extendsVariableInterface;
+ Class<List[]> rawArray;
+ Class<List<String>[]> parameterizedArray;
+ Class<? extends List[]> extendsRawArray;
+ Class<? extends List<String>[]> extendsParameterizedArray;
+ Class<? super List[]> superRawArray;
+ Class<? super List<String>[]> superParameterizedArray;
+ Class<List<? extends List>> nestedRaw;
+ Class<List<? extends List<String>>> nestedParameterized;
+ Class<? extends List<? extends List<String>>>
extendsNestedParameterized;
+ Class<? super List<? extends List>> superNestedRaw;
+ Class<? super List<? extends List<String>>> superNestedParameterized;
+ Class<U> unboundedVariable;
+ Class<? extends U> extendsUnboundedVariable;
+ Class<? super U> superUnboundedVariable;
+ Class<W> dependentVariable;
+ Class<? extends W> extendsDependentVariable;
+ Class<? super W> superDependentVariable;
+ }
+
/**
* A custom {@link ParameterizedType} with a structural {@code
hashCode()}, whose owner and type arguments can be set after construction so
that tests can
* create cycles.
@@ -350,6 +394,29 @@ class Deep<V> {
Map<T, T> map;
}
+ private static class GenericNumberOwner<T> {
+ class Member implements TestIF<Number> {
+ }
+
+ static class StaticMember implements TestIF<Number> {
+ }
+ }
+
+ private static class NumberTestImpl implements TestIF<Number> {
+ }
+
+ private static class ParameterizedRawTestImpl<T> extends TestImpl {
+ }
+
+ private static class RawGenericNumberTestImpl<T> extends NumberTestImpl {
+ }
+
+ private static class RawNumberTestImpl extends RawGenericNumberTestImpl {
+ }
+
+ private static class RawTestImpl extends TestImpl {
+ }
+
public class Tester implements This<String, B> {
// empty
}
@@ -405,6 +472,49 @@ public static <T extends Comparable<? extends T>> T
stub3() {
return null;
}
+ static Stream<Arguments> testIsAssignable_ClassWildcardBounds() {
+ return Stream.of(
+ Arguments.of("extendsRawList", "extendsStrings", false),
+ Arguments.of("extendsRawList", "extendsUnknownList", false),
+ Arguments.of("rawBound", "extendsStrings", false),
+ Arguments.of("rawBound", "extendsUnknownList", false),
+ Arguments.of("extendsRawBound", "extendsStrings", false),
+ Arguments.of("rawSubclass", "extendsUnknownInterface", false),
+ Arguments.of("parameterizedRawSubclass",
"extendsUnknownInterface", false),
+ Arguments.of("rawAncestor", "extendsNumberInterface", false),
+ Arguments.of("rawMember", "extendsNumberInterface", false),
+ Arguments.of("nestedRaw", "extendsNestedParameterized", false),
+ Arguments.of("rawArray", "extendsParameterizedArray", false),
+ Arguments.of("extendsRawArray", "extendsParameterizedArray",
false),
+ Arguments.of("strings", "superRawList", false),
+ Arguments.of("superStrings", "superRawList", false),
+ Arguments.of("parameterizedBound", "superRawList", false),
+ Arguments.of("nestedParameterized", "superNestedRaw", false),
+ Arguments.of("parameterizedArray", "superRawArray", false),
+ Arguments.of("unboundedVariable", "extendsRawBound", false),
+ Arguments.of("unboundedVariable", "extendsDependentVariable",
false),
+ Arguments.of("dependentVariable", "superUnboundedVariable",
false),
+ Arguments.of("rawBound", "extendsUnboundedVariable", false),
+ Arguments.of("rawBound", "superUnboundedVariable", false),
+ Arguments.of("strings", "extendsStrings", true),
+ Arguments.of("stringArrayList", "extendsStrings", true),
+ Arguments.of("parameterizedBound", "extendsStrings", true),
+ Arguments.of("fixedSubclass", "extendsNumberInterface", true),
+ Arguments.of("staticMember", "extendsNumberInterface", true),
+ Arguments.of("parameterizedMember", "extendsNumberInterface",
true),
+ Arguments.of("parameterizedSubclass",
"extendsNumberInterface", true),
+ Arguments.of("variableSubclass", "extendsVariableInterface",
true),
+ Arguments.of("rawList", "superStrings", true),
+ Arguments.of("rawArray", "superParameterizedArray", true),
+ Arguments.of("nestedRaw", "superNestedParameterized", true),
+ Arguments.of("parameterizedArray",
"extendsParameterizedArray", true),
+ Arguments.of("nestedParameterized",
"extendsNestedParameterized", true),
+ Arguments.of("unboundedVariable", "extendsUnboundedVariable",
true),
+ Arguments.of("unboundedVariable", "superUnboundedVariable",
true),
+ Arguments.of("dependentVariable", "extendsUnboundedVariable",
true),
+ Arguments.of("unboundedVariable", "superDependentVariable",
true));
+ }
+
static Stream<Type> testTypeToString() {
// @formatter:off
return Stream.of(Comparator.class, Comparable.class, ArrayList.class,
HashMap.class)
@@ -1109,6 +1219,83 @@ void testIsAssignable_ClassWithParameterizedType3() {
assertFalse(TypeUtils.isAssignable(from3, to3), "Class<TestImpl2>
should not be assignable to Class<? extends TestIF<Number>>");
}
+ @Test
+ void testIsAssignable_ClassWithUnboundedWildcard() {
+ final Type to = TypeUtils.parameterize(Class.class,
TypeUtils.wildcardType().build());
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
Integer.class), to),
+ "Class<Integer> should be assignable to Class<?>");
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
Class.class.getTypeParameters()[0]), to),
+ "Class<T> should be assignable to Class<?>");
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
List.class.getTypeParameters()[0]), to),
+ "Class<E> should be assignable to Class<?> when E is declared
by another class");
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
TypeUtils.wildcardType().withUpperBounds(Number.class).build()), to),
+ "Class<? extends Number> should be assignable to Class<?>");
+ assertTrue(TypeUtils.isAssignable(Class.class, to), "raw Class should
be assignable to Class<?>");
+ assertFalse(TypeUtils.isAssignable(to,
TypeUtils.parameterize(Class.class, Integer.class)),
+ "Class<?> should not be assignable to Class<Integer>");
+ }
+
+ @Test
+ void testIsAssignable_ClassWithBoundedWildcardObject() {
+ final Type to = TypeUtils.parameterize(Class.class,
TypeUtils.wildcardType().withUpperBounds(Object.class).build());
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
String.class), to),
+ "Class<String> should be assignable to Class<? extends
Object>");
+ }
+
+ @Test
+ void testIsAssignable_ClassWithUpperBoundedWildcard() {
+ final Type to = TypeUtils.parameterize(Class.class,
TypeUtils.wildcardType().withUpperBounds(Number.class).build());
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
Integer.class), to),
+ "Class<Integer> should be assignable to Class<? extends
Number>");
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
TypeUtils.wildcardType().withUpperBounds(Integer.class).build()), to),
+ "Class<? extends Integer> should be assignable to Class<?
extends Number>");
+ assertFalse(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
String.class), to),
+ "Class<String> should not be assignable to Class<? extends
Number>");
+ }
+
+ @Test
+ void testIsAssignable_ClassWithLowerBoundedWildcard() {
+ final Type to = TypeUtils.parameterize(Class.class,
TypeUtils.wildcardType().withLowerBounds(Integer.class).build());
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
Number.class), to),
+ "Class<Number> should be assignable to Class<? super
Integer>");
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
TypeUtils.wildcardType().withLowerBounds(Number.class).build()), to),
+ "Class<? super Number> should be assignable to Class<? super
Integer>");
+ assertFalse(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
String.class), to),
+ "Class<String> should not be assignable to Class<? super
Integer>");
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
List.class),
+ TypeUtils.parameterize(Class.class,
TypeUtils.wildcardType().withLowerBounds(TypeUtils.parameterize(List.class,
String.class)).build())),
+ "Class<List> should be assignable to Class<? super
List<String>>");
+ }
+
+ @Test
+ void testIsAssignable_ClassWithParameterizedWildcardBound() {
+ final Type to = TypeUtils.parameterize(Class.class,
+
TypeUtils.wildcardType().withUpperBounds(TypeUtils.parameterize(Collection.class,
TypeUtils.wildcardType().build())).build());
+ assertTrue(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
TypeUtils.parameterize(List.class, String.class)), to),
+ "Class<List<String>> should be assignable to Class<? extends
Collection<?>>");
+ assertFalse(TypeUtils.isAssignable(TypeUtils.parameterize(Class.class,
TypeUtils.parameterize(List.class, String.class)),
+ TypeUtils.parameterize(Class.class, TypeUtils.wildcardType()
+
.withUpperBounds(TypeUtils.parameterize(Collection.class,
Integer.class)).build())),
+ "Class<List<String>> should not be assignable to Class<?
extends Collection<Integer>>");
+ }
+
+ @ParameterizedTest(name = "{0} -> {1}: {2}")
+ @MethodSource
+ void testIsAssignable_ClassWildcardBounds(final String fromField, final
String toField, final boolean expected) throws NoSuchFieldException {
+ final Type from =
ClassWildcardTypes.class.getDeclaredField(fromField).getGenericType();
+ final Type to =
ClassWildcardTypes.class.getDeclaredField(toField).getGenericType();
+ assertEquals(expected, TypeUtils.isAssignable(from, to), () -> from +
" -> " + to);
+ }
+
+ @Test
+ void testIsAssignable_UnrelatedTypeVariables() {
+ final TypeVariable<?>[] variables =
ClassWildcardTypes.class.getTypeParameters();
+ assertFalse(TypeUtils.isAssignable(variables[0], variables[1]));
+ assertFalse(TypeUtils.isAssignable(variables[1], variables[0]));
+ assertFalse(TypeUtils.isAssignable(variables[1], variables[3]));
+ assertTrue(TypeUtils.isAssignable(variables[3], variables[1]));
+ }
+
@Test
public void testIsAssignable_HiddenMapLogic() {
class Container<T> {