From 5562fc348a03579340e57b81e93155eba3eb441a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=C5=A0ari=C4=87?= Date: Tue, 30 Dec 2025 18:06:19 +0100 Subject: [PATCH 1/2] 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. --- .../commons/lang3/reflect/TypeUtils.java | 28 ++++++++++-- .../commons/lang3/reflect/TypeUtilsTest.java | 43 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 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 f1fc44ae3da..3633a26e430 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java @@ -841,7 +841,19 @@ private static Map, Type> getTypeArguments(final ParameterizedTy return typeVarAssigns; } // walk the inheritance hierarchy until the target class is reached - return getTypeArguments(getClosestParentType(cls, toClass), toClass, typeVarAssigns); + final Type parentType = getClosestParentType(cls, toClass); + if (parentType instanceof ParameterizedType) { + 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]); + if (unrolled != null) { + parentTypeArgs[i] = unrolled; + } + } + return getTypeArguments(parameterizeWithOwner(parameterizedParentType.getOwnerType(), (Class) parameterizedParentType.getRawType(), parentTypeArgs), toClass, typeVarAssigns); + } + return getTypeArguments(parentType, toClass, typeVarAssigns); } /** @@ -1672,9 +1684,17 @@ public static Type unrollVariables(Map, Type> typeArguments, fin if (typeArguments == null) { typeArguments = Collections.emptyMap(); } + return unrollVariables(typeArguments, type, new HashSet<>()); + } + + private static Type unrollVariables(final Map, Type> typeArguments, final Type type, final Set> visited) { if (containsTypeVariables(type)) { if (type instanceof TypeVariable) { - return unrollVariables(typeArguments, typeArguments.get(type)); + final TypeVariable var = (TypeVariable) type; + if (!visited.add(var)) { + return var; + } + return unrollVariables(typeArguments, typeArguments.get(type), visited); } if (type instanceof ParameterizedType) { final ParameterizedType p = (ParameterizedType) type; @@ -1685,9 +1705,9 @@ public static Type unrollVariables(Map, Type> typeArguments, fin parameterizedTypeArguments = new HashMap<>(typeArguments); parameterizedTypeArguments.putAll(getTypeArguments(p)); } - final Type[] args = p.getActualTypeArguments(); + final Type[] args = p.getActualTypeArguments().clone(); for (int i = 0; i < args.length; i++) { - final Type unrolled = unrollVariables(parameterizedTypeArguments, args[i]); + final Type unrolled = unrollVariables(parameterizedTypeArguments, args[i], visited); if (unrolled != null) { args[i] = unrolled; } 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 fd3a6ec6565..d4056876483 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java @@ -368,6 +368,49 @@ void test_LANG_1702() throws NoSuchMethodException, SecurityException { final Type unrolledType = TypeUtils.unrollVariables(typeArguments, type); } + static class MyException extends Exception implements Iterable { + private static final long serialVersionUID = 1L; + @Override + public java.util.Iterator iterator() { + return null; + } + } + + static class MyNonTransientException extends MyException { + private static final long serialVersionUID = 1L; + } + + interface MyComparator { + } + + static class MyOrdering implements MyComparator { + } + + static class LexOrdering extends MyOrdering> implements Serializable { + private static final long serialVersionUID = 1L; + } + + /** + * Tests that a parameterized type with a nested generic argument is correctly + * evaluated for assignability to a wildcard lower-bounded type. + * + * @see LANG-1700 + */ + @Test + public void test_LANG_1700() { + final ParameterizedType from = TypeUtils.parameterize(LexOrdering.class, MyNonTransientException.class); + // MyComparator + final ParameterizedType to = TypeUtils.parameterize(MyComparator.class, + TypeUtils.wildcardType().withLowerBounds(MyNonTransientException.class).build()); + + // This is MyComparator> + // It should NOT be assignable to MyComparator + // because Iterable is NOT a supertype of MyNonTransientException + + assertFalse(TypeUtils.isAssignable(from, to), + () -> String.format("Type %s should not be assignable to %s", TypeUtils.toString(from), TypeUtils.toString(to))); + } + @Test void testContainsTypeVariables() throws NoSuchMethodException { assertFalse(TypeUtils.containsTypeVariables(Test1.class.getMethod("m0").getGenericReturnType())); From 3e2c5a848dcef42b39fa781ab3133c8639ab6a66 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Wed, 31 Dec 2025 16:30:46 -0500 Subject: [PATCH 2/2] Fix iterator method signature in MyException class --- .../org/apache/commons/lang3/reflect/TypeUtilsTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 d4056876483..f0355987432 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java @@ -43,6 +43,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; @@ -369,9 +370,11 @@ void test_LANG_1702() throws NoSuchMethodException, SecurityException { } static class MyException extends Exception implements Iterable { + private static final long serialVersionUID = 1L; + @Override - public java.util.Iterator iterator() { + public Iterator iterator() { return null; } } @@ -402,11 +405,9 @@ public void test_LANG_1700() { // MyComparator final ParameterizedType to = TypeUtils.parameterize(MyComparator.class, TypeUtils.wildcardType().withLowerBounds(MyNonTransientException.class).build()); - // This is MyComparator> // It should NOT be assignable to MyComparator // because Iterable is NOT a supertype of MyNonTransientException - assertFalse(TypeUtils.isAssignable(from, to), () -> String.format("Type %s should not be assignable to %s", TypeUtils.toString(from), TypeUtils.toString(to))); }