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()));

Reply via email to