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.
