This is an automated email from the ASF dual-hosted git repository.
ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new 8f8cc04e6 [LANG-1700] Improve handling of parameterized types and
variable unrolling (#1549)
8f8cc04e6 is described below
commit 8f8cc04e65506fbe79bbc5efa6f7d44d65f0dbf1
Author: Ivan Šarić <[email protected]>
AuthorDate: Wed Dec 31 23:06:38 2025 +0100
[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]>
---
.../apache/commons/lang3/reflect/TypeUtils.java | 28 ++++++++++++--
.../commons/lang3/reflect/TypeUtilsTest.java | 44 ++++++++++++++++++++++
2 files changed, 68 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 f1fc44ae3..3633a26e4 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<TypeVariable<?>, 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<TypeVariable<?>,
Type> typeArguments, fin
if (typeArguments == null) {
typeArguments = Collections.emptyMap();
}
+ return unrollVariables(typeArguments, type, new HashSet<>());
+ }
+
+ private static Type unrollVariables(final Map<TypeVariable<?>, Type>
typeArguments, final Type type, final Set<TypeVariable<?>> 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<TypeVariable<?>,
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 fd3a6ec65..f03559874 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;
@@ -368,6 +369,49 @@ void test_LANG_1702() throws NoSuchMethodException,
SecurityException {
final Type unrolledType = TypeUtils.unrollVariables(typeArguments,
type);
}
+ static class MyException extends Exception implements Iterable<Throwable> {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Iterator<Throwable> iterator() {
+ return null;
+ }
+ }
+
+ static class MyNonTransientException extends MyException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ interface MyComparator<T> {
+ }
+
+ static class MyOrdering<T> implements MyComparator<T> {
+ }
+
+ static class LexOrdering<T> extends MyOrdering<Iterable<T>> 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 <a
href="https://issues.apache.org/jira/browse/LANG-1700">LANG-1700</a>
+ */
+ @Test
+ public void test_LANG_1700() {
+ final ParameterizedType from =
TypeUtils.parameterize(LexOrdering.class, MyNonTransientException.class);
+ // MyComparator<? super MyNonTransientException>
+ final ParameterizedType to = TypeUtils.parameterize(MyComparator.class,
+
TypeUtils.wildcardType().withLowerBounds(MyNonTransientException.class).build());
+ // This is MyComparator<Iterable<MyNonTransientException>>
+ // It should NOT be assignable to MyComparator<? super
MyNonTransientException>
+ // because Iterable<MyNonTransientException> 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()));