Skip to content

Commit 8f8cc04

Browse files
isaricgarydgregory
andauthored
[LANG-1700] Improve handling of parameterized types and variable unrolling (#1549)
* 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. * Fix iterator method signature in MyException class --------- Co-authored-by: Gary Gregory <[email protected]>
1 parent 71d4f3d commit 8f8cc04

File tree

2 files changed

+68
-4
lines changed

2 files changed

+68
-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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import java.util.Collections;
4444
import java.util.Comparator;
4545
import java.util.HashMap;
46+
import java.util.Iterator;
4647
import java.util.List;
4748
import java.util.Map;
4849
import java.util.Properties;
@@ -368,6 +369,49 @@ void test_LANG_1702() throws NoSuchMethodException, SecurityException {
368369
final Type unrolledType = TypeUtils.unrollVariables(typeArguments, type);
369370
}
370371

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

0 commit comments

Comments
 (0)