Skip to content

Commit 5562fc3

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 71d4f3d commit 5562fc3

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
/**
@@ -1672,9 +1684,17 @@ public static Type unrollVariables(Map<TypeVariable<?>, Type> typeArguments, fin
16721684
if (typeArguments == null) {
16731685
typeArguments = Collections.emptyMap();
16741686
}
1687+
return unrollVariables(typeArguments, type, new HashSet<>());
1688+
}
1689+
1690+
private static Type unrollVariables(final Map<TypeVariable<?>, Type> typeArguments, final Type type, final Set<TypeVariable<?>> visited) {
16751691
if (containsTypeVariables(type)) {
16761692
if (type instanceof TypeVariable<?>) {
1677-
return unrollVariables(typeArguments, typeArguments.get(type));
1693+
final TypeVariable<?> var = (TypeVariable<?>) type;
1694+
if (!visited.add(var)) {
1695+
return var;
1696+
}
1697+
return unrollVariables(typeArguments, typeArguments.get(type), visited);
16781698
}
16791699
if (type instanceof ParameterizedType) {
16801700
final ParameterizedType p = (ParameterizedType) type;
@@ -1685,9 +1705,9 @@ public static Type unrollVariables(Map<TypeVariable<?>, Type> typeArguments, fin
16851705
parameterizedTypeArguments = new HashMap<>(typeArguments);
16861706
parameterizedTypeArguments.putAll(getTypeArguments(p));
16871707
}
1688-
final Type[] args = p.getActualTypeArguments();
1708+
final Type[] args = p.getActualTypeArguments().clone();
16891709
for (int i = 0; i < args.length; i++) {
1690-
final Type unrolled = unrollVariables(parameterizedTypeArguments, args[i]);
1710+
final Type unrolled = unrollVariables(parameterizedTypeArguments, args[i], visited);
16911711
if (unrolled != null) {
16921712
args[i] = unrolled;
16931713
}

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

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

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

0 commit comments

Comments
 (0)