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
commit 110683aa672b874d7ccecc1b1e89baf7774dd3c4 Author: Gary Gregory <[email protected]> AuthorDate: Thu Oct 15 15:42:22 2026 -0400 Follow up: Fix TypeUtils.isAssignable rejecting Class<X> to Class<?> assignments (#1820) Fix nested type substitution in TypeUtils assignability. Substitute inherited generic array components and preserve external type variables nested in wildcard bounds when traversing parent types. Remove the redundant assignment-map snapshot and correct the Javadoc. Add regression coverage for array substitution and upper and lower wildcard bounds. --- .../apache/commons/lang3/reflect/TypeUtils.java | 122 ++++++++------- .../commons/lang3/reflect/TypeUtilsTest.java | 164 ++++++++++++--------- 2 files changed, 168 insertions(+), 118 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 f56db9e4e..173d99c33 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java @@ -863,7 +863,7 @@ private static Map<TypeVariable<?>, Type> getTypeArguments(final ParameterizedTy final ParameterizedType parameterizedParentType = (ParameterizedType) parentType; final Type[] parentTypeArgs = parameterizedParentType.getActualTypeArguments().clone(); for (int i = 0; i < parentTypeArgs.length; i++) { - final Type unrolled = unrollVariables(typeVarAssigns, parentTypeArgs[i]); + final Type unrolled = unrollVariables(typeVarAssigns, parentTypeArgs[i], new HashSet<>(), newIdentitySet(), true); if (unrolled != null) { parentTypeArgs[i] = unrolled; } @@ -1093,7 +1093,7 @@ private static boolean isAssignable(final Type type, final GenericArrayType toGe * @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}. + * @return {@code true} if {@code type} is assignable to {@code toParameterizedType}. */ private static boolean isAssignable(final Type type, final ParameterizedType toParameterizedType, final Map<TypeVariable<?>, Type> typeVarAssigns, final boolean allowUnchecked) { @@ -1141,13 +1141,8 @@ private static boolean isAssignable(final Type type, final ParameterizedType toP 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)); + // Preserve unmapped variables, including those nested in wildcard bounds, when substituting the parent type. + final Type parent = unrollVariables(fromAssignments, getClosestParentType(fromClass, toClass), new HashSet<>(), newIdentitySet(), true); return isAssignable(parent, toParameterizedType, typeVarAssigns, false); } } @@ -1204,36 +1199,6 @@ && isAssignableToClassWildcard(fromTypeArg, (WildcardType) toTypeArg, typeVarAss 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)}. @@ -1403,6 +1368,21 @@ private static boolean isAssignable(final Type type, final WildcardType toWildca 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 the class contains a cyclical reference in the qualified name of a class. If any of the type parameters of A class is extending X class * which is in scope of A class, then it forms cycle. @@ -1435,6 +1415,21 @@ public static boolean isInstance(final Object value, final Type type) { return value == null ? !(type instanceof Class<?>) || !((Class<?>) type).isPrimitive() : isAssignable(value.getClass(), type, null); } + /** + * 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 whether two type arrays hold the very same types, compared by identity so that no {@code equals()} or {@code hashCode()} of a custom {@link Type} * is called. @@ -1827,17 +1822,19 @@ private static String typeVariableToString(final TypeVariable<?> typeVariable) { /** * Unrolls variables in a type bounds array, preserving cycle state. * - * @param typeArguments assignments {@link Map}. - * @param bounds in which to expand variables. - * @param visited set of visited type variables for cycle detection. - * @param unrolling identity-based types currently being unrolled to prevent recursion cycles. + * @param typeArguments assignments {@link Map}. + * @param bounds in which to expand variables. + * @param visited set of visited type variables for cycle detection. + * @param unrolling identity-based types currently being unrolled to prevent recursion cycles. + * @param preserveUnmapped whether to preserve variables without assignments. * @return {@code bounds} with any variables reassigned. */ - private static Type[] unrollBounds(final Map<TypeVariable<?>, Type> typeArguments, final Type[] bounds, final Set<TypeVariable<?>> visited, final Set<Type> unrolling) { + private static Type[] unrollBounds(final Map<TypeVariable<?>, Type> typeArguments, final Type[] bounds, final Set<TypeVariable<?>> visited, final Set<Type> unrolling, + final boolean preserveUnmapped) { Type[] result = bounds; int i = 0; for (; i < result.length; i++) { - final Type unrolled = unrollVariables(typeArguments, result[i], visited, unrolling); + final Type unrolled = unrollVariables(typeArguments, result[i], visited, unrolling, preserveUnmapped); if (unrolled == null) { result = ArrayUtils.remove(result, i--); } else { @@ -1869,6 +1866,9 @@ private static Type unrollVariableAssignments(TypeVariable<?> typeVariable, fina /** * Gets a type representing {@code type} with variable assignments "unrolled." * + * <p>Substitutes variables in parameterized types, wildcard bounds, and generic array components. If an array component resolves to a class, the result + * is the corresponding array class.</p> + * * @param typeArguments as from {@link TypeUtils#getTypeArguments(Type, Class)}. * @param type The type to unroll variable assignments for. * @return Type. @@ -1878,22 +1878,39 @@ public static Type unrollVariables(Map<TypeVariable<?>, Type> typeArguments, fin if (typeArguments == null) { typeArguments = Collections.emptyMap(); } - return unrollVariables(typeArguments, type, new HashSet<>(), newIdentitySet()); + return unrollVariables(typeArguments, type, new HashSet<>(), newIdentitySet(), false); } - private static Type unrollVariables(final Map<TypeVariable<?>, Type> typeArguments, final Type type, final Set<TypeVariable<?>> visited, final Set<Type> unrolling) { + private static Type unrollVariables(final Map<TypeVariable<?>, Type> typeArguments, final Type type, final Set<TypeVariable<?>> visited, final Set<Type> unrolling, + final boolean preserveUnmapped) { if (type instanceof TypeVariable<?>) { final TypeVariable<?> var = (TypeVariable<?>) type; if (!visited.add(var)) { return var; } try { - return unrollVariables(typeArguments, typeArguments.get(type), visited, unrolling); + final Type assignment = typeArguments.get(type); + return assignment == null && preserveUnmapped ? var : unrollVariables(typeArguments, assignment, visited, unrolling, preserveUnmapped); } finally { // Only guard against cycles: the same variable may legitimately occur more than once in a type. visited.remove(var); } } + if (type instanceof GenericArrayType) { + if (!unrolling.add(type)) { + return type; + } + try { + final Type component = ((GenericArrayType) type).getGenericComponentType(); + final Type unrolled = unrollVariables(typeArguments, component, visited, unrolling, preserveUnmapped); + if (unrolled == null || unrolled == component) { + return type; + } + return unrolled instanceof Class<?> ? Array.newInstance((Class<?>) unrolled, 0).getClass() : genericArrayType(unrolled); + } finally { + unrolling.remove(type); + } + } if (type instanceof ParameterizedType) { final ParameterizedType p = (ParameterizedType) type; if (hasCyclicOwnerChain(p) || !unrolling.add(p)) { @@ -1904,14 +1921,15 @@ private static Type unrollVariables(final Map<TypeVariable<?>, Type> typeArgumen final Type[] unrolledArgs = args.clone(); boolean changed = false; for (int i = 0; i < args.length; i++) { - final Type unrolled = unrollVariables(typeArguments, args[i], visited, unrolling); + final Type unrolled = unrollVariables(typeArguments, args[i], visited, unrolling, preserveUnmapped); if (unrolled != null && unrolled != args[i]) { unrolledArgs[i] = unrolled; changed = true; } } final Type owner = p.getOwnerType(); - final Type unrolledOwner = owner != null && !(owner instanceof Class<?>) ? unrollVariables(typeArguments, owner, visited, unrolling) : owner; + final Type unrolledOwner = owner != null && !(owner instanceof Class<?>) + ? unrollVariables(typeArguments, owner, visited, unrolling, preserveUnmapped) : owner; if (!changed && unrolledOwner == owner) { return p; } @@ -1928,8 +1946,8 @@ private static Type unrollVariables(final Map<TypeVariable<?>, Type> typeArgumen try { final Type[] upperBounds = wild.getUpperBounds(); final Type[] lowerBounds = wild.getLowerBounds(); - final Type[] unrolledUpperBounds = unrollBounds(typeArguments, upperBounds.clone(), visited, unrolling); - final Type[] unrolledLowerBounds = unrollBounds(typeArguments, lowerBounds.clone(), visited, unrolling); + final Type[] unrolledUpperBounds = unrollBounds(typeArguments, upperBounds.clone(), visited, unrolling, preserveUnmapped); + final Type[] unrolledLowerBounds = unrollBounds(typeArguments, lowerBounds.clone(), visited, unrolling, preserveUnmapped); if (isSameTypes(upperBounds, unrolledUpperBounds) && isSameTypes(lowerBounds, unrolledLowerBounds)) { return wild; } 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 f68f0175d..284ddddea 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java @@ -225,15 +225,19 @@ public interface And<K, V> extends This<Number, Number> { // empty } - public static class ClassWithSuperClassWithGenericType extends ArrayList<Object> { + private static class ArrayValues<T> extends ArrayList<T[]> { private static final long serialVersionUID = 1L; - - public static <U> Iterable<U> methodWithGenericReturnType() { - return null; - } } private static class ClassWildcardTypes<T extends List, U, V extends List<String>, W extends U> { + Class<ArrayValues<String>> arrayValues; + Class<ArrayValues<U>> variableArrayValues; + Class<? extends List<String[]>> extendsStringArrays; + Class<? extends List<U[]>> extendsVariableArrays; + Class<ArrayList<? extends U>> upperVariableValues; + Class<ArrayList<? super U>> lowerVariableValues; + Class<? extends List<? extends U>> extendsUpperVariableValues; + Class<? extends List<? super U>> extendsLowerVariableValues; Class<List<String>> strings; Class<List> rawList; Class<ArrayList<String>> stringArrayList; @@ -276,6 +280,14 @@ private static class ClassWildcardTypes<T extends List, U, V extends List<String Class<? super W> superDependentVariable; } + public static class ClassWithSuperClassWithGenericType extends ArrayList<Object> { + private static final long serialVersionUID = 1L; + + public static <U> Iterable<U> methodWithGenericReturnType() { + return null; + } + } + /** * 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. @@ -331,6 +343,14 @@ public String toString() { } } + private static class GenericNumberOwner<T> { + class Member implements TestIF<Number> { + } + + static class StaticMember implements TestIF<Number> { + } + } + static class LexOrdering<T> extends MyOrdering<Iterable<T>> implements Serializable { private static final long serialVersionUID = 1L; } @@ -363,6 +383,9 @@ static class MyNonTransientException extends MyException { static class MyOrdering<T> implements MyComparator<T> { } + private static class NumberTestImpl implements TestIF<Number> { + } + public class Other<T> implements This<String, T> { // empty } @@ -394,17 +417,6 @@ 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 { } @@ -474,6 +486,13 @@ public static <T extends Comparable<? extends T>> T stub3() { static Stream<Arguments> testIsAssignable_ClassWildcardBounds() { return Stream.of( + Arguments.of("arrayValues", "extendsStringArrays", true), + Arguments.of("variableArrayValues", "extendsVariableArrays", true), + Arguments.of("arrayValues", "extendsVariableArrays", false), + Arguments.of("upperVariableValues", "extendsUpperVariableValues", true), + Arguments.of("lowerVariableValues", "extendsLowerVariableValues", true), + Arguments.of("upperVariableValues", "extendsLowerVariableValues", false), + Arguments.of("lowerVariableValues", "extendsUpperVariableValues", false), Arguments.of("extendsRawList", "extendsStrings", false), Arguments.of("extendsRawList", "extendsUnknownList", false), Arguments.of("rawBound", "extendsStrings", false), @@ -1195,6 +1214,35 @@ void testIsArrayTypeClasses() { assertFalse(TypeUtils.isArrayType(String.class)); } + @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_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_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_ClassWithParameterizedType1() { final ParameterizedType topre1 = TypeUtils.parameterize(TestIF.class, TypeUtils.wildcardType().build()); @@ -1219,6 +1267,18 @@ void testIsAssignable_ClassWithParameterizedType3() { assertFalse(TypeUtils.isAssignable(from3, to3), "Class<TestImpl2> should not be assignable to Class<? extends TestIF<Number>>"); } + @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>>"); + } + @Test void testIsAssignable_ClassWithUnboundedWildcard() { final Type to = TypeUtils.parameterize(Class.class, TypeUtils.wildcardType().build()); @@ -1235,13 +1295,6 @@ void testIsAssignable_ClassWithUnboundedWildcard() { "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()); @@ -1253,49 +1306,6 @@ void testIsAssignable_ClassWithUpperBoundedWildcard() { "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> { @@ -1315,6 +1325,15 @@ class Container<T> { assertFalse(result); } + @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 void testIsAssignableClasses() { assertTrue(TypeUtils.isAssignable(char.class, double.class)); @@ -1987,6 +2006,19 @@ void testUnrollVariablesCyclicOwnerChainFromEitherType() { assertSame(b, TypeUtils.unrollVariables(mapping, b)); } + @Test + void testUnrollVariablesGenericArray() { + final TypeVariable<?> variable = ArrayValues.class.getTypeParameters()[0]; + final GenericArrayType array = TypeUtils.genericArrayType(variable); + assertSame(array, TypeUtils.unrollVariables(Collections.emptyMap(), array)); + assertSame(array, TypeUtils.unrollVariables(Collections.singletonMap(variable, variable), array)); + assertEquals(String[].class, TypeUtils.unrollVariables(Collections.singletonMap(variable, String.class), array)); + final Type list = TypeUtils.parameterize(List.class, String.class); + assertEquals(TypeUtils.genericArrayType(list), TypeUtils.unrollVariables(Collections.singletonMap(variable, list), array)); + assertEquals(String[][].class, + TypeUtils.unrollVariables(Collections.singletonMap(variable, String.class), TypeUtils.genericArrayType(array))); + } + @Test void testUnrollVariablesMultiLevelOwnerType() throws NoSuchFieldException { final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
