Skip to content

Commit abae510

Browse files
committed
LANG-1700 Improve handling of parameterized types and variable unrolling
Enhanced `TypeUtils` to correctly handle parameterized types with nested generic arguments and improve unrolling of type variables. Updated `unrollVariables` to prevent infinite recursion by handling visited `TypeVariable` instances. Modified argument cloning to avoid in-place mutations. Added unit tests to validate behavior against complex parameterized types and ensure accurate assignability checks.
1 parent 5e9736a commit abae510

File tree

2 files changed

+67
-4
lines changed

2 files changed

+67
-4
lines changed

src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,19 @@ private static Map<TypeVariable<?>, Type> getTypeArguments(final ParameterizedTy
841841
return typeVarAssigns;
842842
}
843843
// walk the inheritance hierarchy until the target class is reached
844-
return getTypeArguments(getClosestParentType(cls, toClass), toClass, typeVarAssigns);
844+
final Type parentType = getClosestParentType(cls, toClass);
845+
if (parentType instanceof ParameterizedType) {
846+
final ParameterizedType parameterizedParentType = (ParameterizedType) parentType;
847+
final Type[] parentTypeArgs = parameterizedParentType.getActualTypeArguments().clone();
848+
for (int i = 0; i < parentTypeArgs.length; i++) {
849+
final Type unrolled = unrollVariables(typeVarAssigns, parentTypeArgs[i]);
850+
if (unrolled != null) {
851+
parentTypeArgs[i] = unrolled;
852+
}
853+
}
854+
return getTypeArguments(parameterizeWithOwner(parameterizedParentType.getOwnerType(), (Class<?>) parameterizedParentType.getRawType(), parentTypeArgs), toClass, typeVarAssigns);
855+
}
856+
return getTypeArguments(parentType, toClass, typeVarAssigns);
845857
}
846858

847859
/**
@@ -1661,9 +1673,17 @@ public static Type unrollVariables(Map<TypeVariable<?>, Type> typeArguments, fin
16611673
if (typeArguments == null) {
16621674
typeArguments = Collections.emptyMap();
16631675
}
1676+
return unrollVariables(typeArguments, type, new HashSet<>());
1677+
}
1678+
1679+
private static Type unrollVariables(final Map<TypeVariable<?>, Type> typeArguments, final Type type, final Set<TypeVariable<?>> visited) {
16641680
if (containsTypeVariables(type)) {
16651681
if (type instanceof TypeVariable<?>) {
1666-
return unrollVariables(typeArguments, typeArguments.get(type));
1682+
final TypeVariable<?> var = (TypeVariable<?>) type;
1683+
if (!visited.add(var)) {
1684+
return var;
1685+
}
1686+
return unrollVariables(typeArguments, typeArguments.get(type), visited);
16671687
}
16681688
if (type instanceof ParameterizedType) {
16691689
final ParameterizedType p = (ParameterizedType) type;
@@ -1674,9 +1694,9 @@ public static Type unrollVariables(Map<TypeVariable<?>, Type> typeArguments, fin
16741694
parameterizedTypeArguments = new HashMap<>(typeArguments);
16751695
parameterizedTypeArguments.putAll(getTypeArguments(p));
16761696
}
1677-
final Type[] args = p.getActualTypeArguments();
1697+
final Type[] args = p.getActualTypeArguments().clone();
16781698
for (int i = 0; i < args.length; i++) {
1679-
final Type unrolled = unrollVariables(parameterizedTypeArguments, args[i]);
1699+
final Type unrolled = unrollVariables(parameterizedTypeArguments, args[i], visited);
16801700
if (unrolled != null) {
16811701
args[i] = unrolled;
16821702
}

src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,49 @@ void test_LANG_1702() throws NoSuchMethodException, SecurityException {
359359
final Type unrolledType = TypeUtils.unrollVariables(typeArguments, type);
360360
}
361361

362+
static class MyException extends Exception implements Iterable<Throwable> {
363+
private static final long serialVersionUID = 1L;
364+
@Override
365+
public java.util.Iterator<Throwable> iterator() {
366+
return null;
367+
}
368+
}
369+
370+
static class MyNonTransientException extends MyException {
371+
private static final long serialVersionUID = 1L;
372+
}
373+
374+
interface MyComparator<T> {
375+
}
376+
377+
static class MyOrdering<T> implements MyComparator<T> {
378+
}
379+
380+
static class LexOrdering<T> extends MyOrdering<Iterable<T>> implements Serializable {
381+
private static final long serialVersionUID = 1L;
382+
}
383+
384+
/**
385+
* Tests that a parameterized type with a nested generic argument is correctly
386+
* evaluated for assignability to a wildcard lower-bounded type.
387+
*
388+
* @see <a href="https://issues.apache.org/jira/browse/LANG-1700">LANG-1700</a>
389+
*/
390+
@Test
391+
public void test_LANG_1700() {
392+
final ParameterizedType from = TypeUtils.parameterize(LexOrdering.class, MyNonTransientException.class);
393+
// MyComparator<? super MyNonTransientException>
394+
final ParameterizedType to = TypeUtils.parameterize(MyComparator.class,
395+
TypeUtils.wildcardType().withLowerBounds(MyNonTransientException.class).build());
396+
397+
// This is MyComparator<Iterable<MyNonTransientException>>
398+
// It should NOT be assignable to MyComparator<? super MyNonTransientException>
399+
// because Iterable<MyNonTransientException> is NOT a supertype of MyNonTransientException
400+
401+
assertFalse(TypeUtils.isAssignable(from, to),
402+
() -> String.format("Type %s should not be assignable to %s", TypeUtils.toString(from), TypeUtils.toString(to)));
403+
}
404+
362405
@Test
363406
void testContainsTypeVariables() throws NoSuchMethodException {
364407
assertFalse(TypeUtils.containsTypeVariables(Test1.class.getMethod("m0").getGenericReturnType()));

0 commit comments

Comments
 (0)