This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git

commit dd2c9e85784b288c7507c3736818040b3b457842
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Oct 10 15:30:40 2026 -0400

    [LANG-1836] Make TypeUtils.containsTypeVariables and unrollVariables
    honor the owner type of a ParameterizedType (#1812)
    
    Add TypeUtils regression tests for variable unrolling
    
    Cover wildcard bound substitution and input preservation, unchanged
    wildcard identity, self-referential and mutually cyclic variable
    assignments, and variable detection and substitution across multiple
    owner levels.
---
 src/changes/changes.xml                            |  1 +
 .../commons/lang3/reflect/TypeUtilsTest.java       | 87 +++++++++++++++++++++-
 2 files changed, 87 insertions(+), 1 deletion(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index c7de09c07..25995c819 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -51,6 +51,7 @@ The <action> type attribute can be add,update,fix,remove.
     <action                   type="fix" dev="ggregory" due-to="alhuda, Gary 
Gregory">Take FastDatePrinter era names from the locale's calendar 
(#1800).</action>
     <action issue="LANG-1655" type="fix" dev="ggregory" due-to="Efe, Gary 
Gregory">Preserve Korean text in stripAccents (#1799).</action>
     <action issue="LANG-1835" type="fix" dev="ggregory" due-to="William 
Degrange, zemiles, Gary Gregory">Ignore synthetic methods in AnnotationUtils 
(#1810).</action>
+    <action issue="LANG-1836" type="fix" dev="ggregory" due-to="William 
Degrange, Sharang Gupta, Gary Gregory">Make TypeUtils.containsTypeVariables and 
unrollVariables honour the owner type of a ParameterizedType (#1812).</action>
     <!-- ADD -->
     <!-- UPDATE -->
   </release>
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 19efa56c5..73a2ed080 100644
--- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
@@ -331,7 +331,12 @@ class Inner {
         }
 
         class InnerU<U> {
-            // empty
+
+            class Deep<V> {
+                // empty
+            }
+
+            Deep<T> deep;
         }
 
         Map.Entry<T, Integer> entry;
@@ -600,6 +605,22 @@ void testContainsTypeVariablesMultiBoundWildcard() {
         assertFalse(TypeUtils.containsTypeVariables(wtNone));
     }
 
+    @Test
+    void testContainsTypeVariablesMultiLevelOwnerType() throws 
NoSuchFieldException {
+        final Type type = 
Outer.InnerU.class.getDeclaredField("deep").getGenericType();
+        assertTrue(TypeUtils.containsTypeVariables(type));
+        final Type outer = TypeUtils.parameterize(Outer.class, String.class);
+        final Type inner = TypeUtils.parameterizeWithOwner(outer, 
Outer.InnerU.class, Integer.class);
+        
assertFalse(TypeUtils.containsTypeVariables(TypeUtils.parameterizeWithOwner(inner,
 Outer.InnerU.Deep.class, String.class)));
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final TypeVariable<?> u = Outer.InnerU.class.getTypeParameters()[0];
+        final Type variableOuter = TypeUtils.parameterize(Outer.class, t);
+        final Type outerOnly = TypeUtils.parameterizeWithOwner(variableOuter, 
Outer.InnerU.class, Integer.class);
+        
assertTrue(TypeUtils.containsTypeVariables(TypeUtils.parameterizeWithOwner(outerOnly,
 Outer.InnerU.Deep.class, String.class)));
+        final Type innerOnly = TypeUtils.parameterizeWithOwner(outer, 
Outer.InnerU.class, u);
+        
assertTrue(TypeUtils.containsTypeVariables(TypeUtils.parameterizeWithOwner(innerOnly,
 Outer.InnerU.Deep.class, String.class)));
+    }
+
     @Test
     void testContainsTypeVariablesMutuallyReferencingTypeArguments() {
         // LANG-1836
@@ -1779,6 +1800,34 @@ void testUnrollVariablesCyclicOwnerChainFromEitherType() 
{
         assertSame(b, TypeUtils.unrollVariables(mapping, b));
     }
 
+    @Test
+    void testUnrollVariablesMultiLevelOwnerType() throws NoSuchFieldException {
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final TypeVariable<?> u = Outer.InnerU.class.getTypeParameters()[0];
+        final Map<TypeVariable<?>, Type> mapping = new HashMap<>();
+        mapping.put(t, String.class);
+        mapping.put(u, Integer.class);
+        final Type original = 
Outer.InnerU.class.getDeclaredField("deep").getGenericType();
+        final Type outer = TypeUtils.parameterize(Outer.class, String.class);
+        final Type inner = TypeUtils.parameterizeWithOwner(outer, 
Outer.InnerU.class, Integer.class);
+        final Type expected = TypeUtils.parameterizeWithOwner(inner, 
Outer.InnerU.Deep.class, String.class);
+        assertEquals(expected, TypeUtils.unrollVariables(mapping, original));
+        assertTrue(TypeUtils.containsTypeVariables(original));
+    }
+
+    @Test
+    void testUnrollVariablesMutuallyCyclicAssignments() {
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final TypeVariable<?> u = Outer.InnerU.class.getTypeParameters()[0];
+        final Map<TypeVariable<?>, Type> mapping = new HashMap<>();
+        mapping.put(t, u);
+        mapping.put(u, t);
+        assertSame(t, TypeUtils.unrollVariables(mapping, t));
+        assertSame(u, TypeUtils.unrollVariables(mapping, u));
+        final Type repeated = TypeUtils.parameterize(Map.class, t, t);
+        assertSame(repeated, TypeUtils.unrollVariables(mapping, repeated));
+    }
+
     @Test
     void testUnrollVariablesOwnerType() throws NoSuchFieldException {
         // LANG-1836
@@ -1818,6 +1867,24 @@ void 
testUnrollVariablesReturnsTheSameTypeWhenNothingChanges() {
         assertSame(listOfString, 
TypeUtils.unrollVariables(Collections.emptyMap(), listOfString));
     }
 
+    @Test
+    void testUnrollVariablesReturnsTheSameWildcardWhenNothingChanges() {
+        final WildcardType concrete = 
TypeUtils.wildcardType().withUpperBounds(Number.class).build();
+        assertSame(concrete, TypeUtils.unrollVariables(Collections.emptyMap(), 
concrete));
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final WildcardType variable = 
TypeUtils.wildcardType().withLowerBounds(t).build();
+        assertSame(variable, 
TypeUtils.unrollVariables(Collections.singletonMap(t, t), variable));
+    }
+
+    @Test
+    void testUnrollVariablesSelfCyclicAssignment() {
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final Map<TypeVariable<?>, Type> mapping = Collections.singletonMap(t, 
t);
+        assertSame(t, TypeUtils.unrollVariables(mapping, t));
+        final Type repeated = TypeUtils.parameterize(Map.class, t, t);
+        assertSame(repeated, TypeUtils.unrollVariables(mapping, repeated));
+    }
+
     @Test
     void testUnrollVariablesSelfOwnedTypeWithTypeVariable() {
         // LANG-1836
@@ -1840,6 +1907,24 @@ void testUnrollVariablesSelfReferencingTypeArgument() {
         assertEquals(String.class, unrolledArguments[1]);
     }
 
+    @Test
+    void testUnrollVariablesWildcardBounds() {
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final Map<TypeVariable<?>, Type> mapping = Collections.singletonMap(t, 
String.class);
+        final WildcardType upper = 
TypeUtils.wildcardType().withUpperBounds(t).build();
+        final WildcardType lower = 
TypeUtils.wildcardType().withLowerBounds(t).build();
+        final Type original = TypeUtils.parameterize(Map.class, upper, lower);
+        final Type expected = TypeUtils.parameterize(Map.class,
+                TypeUtils.wildcardType().withUpperBounds(String.class).build(),
+                
TypeUtils.wildcardType().withLowerBounds(String.class).build());
+        assertEquals(expected, TypeUtils.unrollVariables(mapping, original));
+        assertArrayEquals(new Type[] { t }, upper.getUpperBounds());
+        assertArrayEquals(new Type[0], upper.getLowerBounds());
+        assertArrayEquals(new Type[] { Object.class }, lower.getUpperBounds());
+        assertArrayEquals(new Type[] { t }, lower.getLowerBounds());
+        assertArrayEquals(new Type[] { upper, lower }, ((ParameterizedType) 
original).getActualTypeArguments());
+    }
+
     @Test
     void testUnrollVariablesWildcardCycleThroughOwnerType() {
         // LANG-1836: {T -> Outer<? extends T>.Inner} must not cause a 
StackOverflowError when T is unrolled.

Reply via email to