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


The following commit(s) were added to refs/heads/master by this push:
     new 48bf95fb1 [LANG-1836] Make TypeUtils.containsTypeVariables and 
unrollVariables honour the owner type of a ParameterizedType (#1812)
48bf95fb1 is described below

commit 48bf95fb1326a0fb36254028095bab822dc054af
Author: Sharang Gupta <[email protected]>
AuthorDate: Sat Oct 10 20:23:04 2026 +0100

    [LANG-1836] Make TypeUtils.containsTypeVariables and unrollVariables honour 
the owner type of a ParameterizedType (#1812)
    
    * [LANG-1836] Make TypeUtils.containsTypeVariables and unrollVariables 
honour the owner type of a ParameterizedType
    
    containsTypeVariables(Type) only inspected getActualTypeArguments() of a
    ParameterizedType, so Outer<T>.Inner was reported as free of type
    variables although its Javadoc promises a recursive check of the type's
    parameterization. Now the owner type is checked as well, unless it is a
    raw Class (for example Map.class owning Map.Entry<String, Integer>),
    because the Class branch reports true for any class that merely declares
    type parameters.
    
    unrollVariables(Map, Type) had three related problems in its
    ParameterizedType branch:
    
    - The owner type was never unrolled, so Outer<T>.Inner<Integer> kept the
      unresolved owner even when T was mapped. The owner is now unrolled like
      the type arguments (again leaving a raw Class owner untouched) and the
      result is built with parameterizeWithOwner(unrolledOwner, raw, args).
    
    - When an owner was present, getTypeArguments(p) was merged over the
      caller's map with putAll. For Outer<T>.Inner<T> that injects the
      identity entry T -> T, which silently discards the caller's T -> String.
      The merge is dropped: the actual type arguments of p are written in
      terms of the enclosing scope's variables, which the caller's map is
      responsible for, not in terms of the raw type's own parameters.
    
    - The "visited" guard introduced for LANG-1700 never released a variable
      after it had been resolved, so a variable occurring more than once was
      only unrolled the first time (Map<T, T> became Map<String, T>). The
      variable is now removed from the set after its substitution has been
      unrolled, which still breaks genuine cycles such as T -> U -> T.
    
    Add regression tests for Outer<T>.Inner, Outer<String>.Inner,
    Map.Entry<String, Integer>, Outer<T>.Inner<Integer>, Outer<T>.Inner<T>,
    Map.Entry<T, Integer> and Map<T, T>, and a changes.xml entry.
    
    * [LANG-1836] Fix cyclic owner traversal and wildcard owner unrolling
    
    Add cycle tracking to containsTypeVariables() to prevent StackOverflowError
    when a ParameterizedType has self-referential or mutually cyclic owners.
    
    Pass the visited set through unrollBounds() to prevent wildcard owner
    unrolling from losing cycle state, which caused StackOverflowError with
    assignments like {T -> Outer<? extends T>.Inner}.
    
    - containsTypeVariables(): Track visited types during owner traversal
    - unrollBounds(): Add overload accepting visited set for cycle preservation
    - unrollVariables(): Pass visited set to unrollBounds() for wildcard bounds
    
    * [LANG-1836] Remove unused unrollBounds overload and test cyclic owner 
types
    
    The previous commit added unrollBounds(Map, Type[], Set) and left the
    two argument variant without callers, which fails the SpotBugs check
    (UPM_UNCALLED_PRIVATE_METHOD). Remove the unused private method.
    
    Add regression tests for the cycle guards of that commit: a
    ParameterizedType that is its own owner, and an assignment of
    T to Outer<? extends T>.Inner. Both fail with a StackOverflowError
    without the guards.
    
    * [LANG-1836] Harden TypeUtils owner type handling against cyclic types
    
    A custom ParameterizedType can have a cyclic owner chain, or be reached
    again through its own type arguments, wildcard bounds or owners. Cycle
    detection must not rely on hashCode() and equals() of such types, and
    unrolling must not follow a cycle forever.
    
    - containsTypeVariables(Type) detects cycles with an identity based set,
      so it no longer calls the hashCode() of a custom owner type.
    - unrollVariables(Map, Type) leaves a parameterized type with a cyclic
      owner chain unchanged and tracks the parameterized types and wildcard
      types it is currently unrolling by identity.
    - Add tests for self owned types, owner chains that lead back to the
      start from either type, and a cycle through a type argument.
    
    * Javadoc: Enhance unrollBounds with unrolling parameter
    
    Add parameter 'unrolling' to unrollBounds method for recursion prevention.
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * [LANG-1836] Guard TypeUtils.containsTypeVariables against type argument 
cycles
    
    - Track every visited composite type by identity, so a custom
      ParameterizedType, WildcardType or GenericArrayType that reaches itself
      through its type arguments no longer overflows the stack.
    - Skip the identity set in unrollVariables for plain type variables and
      classes, which need no containment check.
    
    * Refactor unrollVariables to handle visited removal safely
    
    Refactor unrollVariables method to use try-finally for visited removal.
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * [LANG-1836] Unroll type variables in a single pass
    
    - Drop the containsTypeVariables pre-checks from unrollVariables, which
      walked the same nested types again at every level. Parameterized and
      wildcard types are now unrolled once and returned as they are when
      nothing changed.
    - A cycle through a type argument and an owner is now left as it is
      instead of being rejected by the rebuild with an
      IllegalArgumentException, like the other cyclic cases.
    
    ---------
    
    Co-authored-by: Gary Gregory <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../apache/commons/lang3/reflect/TypeUtils.java    | 159 ++++++++++---
 .../commons/lang3/reflect/TypeUtilsTest.java       | 261 +++++++++++++++++++++
 2 files changed, 384 insertions(+), 36 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 74feb8848..ecd4dfe64 100644
--- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
+++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
@@ -371,42 +371,66 @@ private static <T> String classToString(final Class<T> 
cls) {
 
     /**
      * Tests, recursively, whether any of the type parameters associated with 
{@code type} are bound to variables.
+     * <p>
+     * The owner type of a parameterized type, for example {@code Outer<T>} of 
{@code Outer<T>.Inner}, is checked as well.
+     * </p>
      *
      * @param type The type to check for type variables.
      * @return Whether any of the type parameters associated with {@code type} 
are bound to variables.
      * @since 3.2
      */
     public static boolean containsTypeVariables(final Type type) {
+        return containsTypeVariables(type, newIdentitySet());
+    }
+
+    /**
+     * Creates a set that compares by identity, so that cycle detection never 
calls {@code hashCode()} or {@code equals()} of a custom {@link Type}.
+     *
+     * @return A new identity-based set.
+     */
+    private static Set<Type> newIdentitySet() {
+        return Collections.newSetFromMap(new IdentityHashMap<>());
+    }
+
+    private static boolean containsTypeVariables(final Type type, final 
Set<Type> visited) {
         if (type instanceof TypeVariable<?>) {
             return true;
         }
         if (type instanceof Class<?>) {
             return ((Class<?>) type).getTypeParameters().length > 0;
         }
+        if (!visited.add(type)) {
+            // A custom Type can contain itself, through its owner or its 
arguments. A type seen before, or still being examined, holds no variable that
+            // would not have ended the search already.
+            return false;
+        }
         if (type instanceof ParameterizedType) {
-            for (final Type arg : ((ParameterizedType) 
type).getActualTypeArguments()) {
-                if (containsTypeVariables(arg)) {
+            final ParameterizedType parameterizedType = (ParameterizedType) 
type;
+            for (final Type arg : parameterizedType.getActualTypeArguments()) {
+                if (containsTypeVariables(arg, visited)) {
                     return true;
                 }
             }
-            return false;
+            // A raw Class owner (for example Map.class for Map.Entry<String, 
Integer>) binds no variables.
+            final Type ownerType = parameterizedType.getOwnerType();
+            return ownerType != null && !(ownerType instanceof Class<?>) && 
containsTypeVariables(ownerType, visited);
         }
         if (type instanceof WildcardType) {
             final WildcardType wild = (WildcardType) type;
             for (final Type bound : getImplicitLowerBounds(wild)) {
-                if (containsTypeVariables(bound)) {
+                if (containsTypeVariables(bound, visited)) {
                     return true;
                 }
             }
             for (final Type bound : getImplicitUpperBounds(wild)) {
-                if (containsTypeVariables(bound)) {
+                if (containsTypeVariables(bound, visited)) {
                     return true;
                 }
             }
             return false;
         }
         if (type instanceof GenericArrayType) {
-            return containsTypeVariables(((GenericArrayType) 
type).getGenericComponentType());
+            return containsTypeVariables(((GenericArrayType) 
type).getGenericComponentType(), visited);
         }
         return false;
     }
@@ -1667,17 +1691,19 @@ private static String typeVariableToString(final 
TypeVariable<?> typeVariable) {
     }
 
     /**
-     * Unrolls variables in a type bounds array.
+     * Unrolls variables in a type bounds array, preserving cycle state.
      *
      * @param typeArguments assignments {@link Map}.
      * @param bounds        in which to expand variables.
+     * @param visited       set of visited type variables for cycle detection.
+     * @param unrolling     identity-based types currently being unrolled to 
prevent recursion cycles.
      * @return {@code bounds} with any variables reassigned.
      */
-    private static Type[] unrollBounds(final Map<TypeVariable<?>, Type> 
typeArguments, final Type[] bounds) {
+    private static Type[] unrollBounds(final Map<TypeVariable<?>, Type> 
typeArguments, final Type[] bounds, final Set<TypeVariable<?>> visited, final 
Set<Type> unrolling) {
         Type[] result = bounds;
         int i = 0;
         for (; i < result.length; i++) {
-            final Type unrolled = unrollVariables(typeArguments, result[i]);
+            final Type unrolled = unrollVariables(typeArguments, result[i], 
visited, unrolling);
             if (unrolled == null) {
                 result = ArrayUtils.remove(result, i--);
             } else {
@@ -1718,45 +1744,106 @@ public static Type 
unrollVariables(Map<TypeVariable<?>, Type> typeArguments, fin
         if (typeArguments == null) {
             typeArguments = Collections.emptyMap();
         }
-        return unrollVariables(typeArguments, type, new HashSet<>());
+        return unrollVariables(typeArguments, type, new HashSet<>(), 
newIdentitySet());
     }
 
-    private static Type unrollVariables(final Map<TypeVariable<?>, Type> 
typeArguments, final Type type, final Set<TypeVariable<?>> visited) {
-        if (containsTypeVariables(type)) {
-            if (type instanceof TypeVariable<?>) {
-                final TypeVariable<?> var = (TypeVariable<?>) type;
-                if (!visited.add(var)) {
-                    return var;
-                }
-                return unrollVariables(typeArguments, typeArguments.get(type), 
visited);
+    /**
+     * Tests whether following the owner types of the given type leads back to 
a type that was already seen. Only a custom {@link ParameterizedType} can do 
that.
+     *
+     * @param type The type whose owner chain to check.
+     * @return Whether the owner chain of {@code type} is cyclic.
+     */
+    private static boolean hasCyclicOwnerChain(final ParameterizedType type) {
+        final Set<Type> seen = newIdentitySet();
+        seen.add(type);
+        for (Type owner = type.getOwnerType(); owner instanceof 
ParameterizedType; owner = ((ParameterizedType) owner).getOwnerType()) {
+            if (!seen.add(owner)) {
+                return true;
             }
-            if (type instanceof ParameterizedType) {
-                final ParameterizedType p = (ParameterizedType) type;
-                final Map<TypeVariable<?>, Type> parameterizedTypeArguments;
-                if (p.getOwnerType() == null) {
-                    parameterizedTypeArguments = typeArguments;
-                } else {
-                    parameterizedTypeArguments = new HashMap<>(typeArguments);
-                    parameterizedTypeArguments.putAll(getTypeArguments(p));
-                }
-                final Type[] args = p.getActualTypeArguments().clone();
+        }
+        return false;
+    }
+
+    private static Type unrollVariables(final Map<TypeVariable<?>, Type> 
typeArguments, final Type type, final Set<TypeVariable<?>> visited, final 
Set<Type> unrolling) {
+        if (type instanceof TypeVariable<?>) {
+            final TypeVariable<?> var = (TypeVariable<?>) type;
+            if (!visited.add(var)) {
+                return var;
+            }
+            try {
+                return unrollVariables(typeArguments, typeArguments.get(type), 
visited, unrolling);
+            } finally {
+                // Only guard against cycles: the same variable may 
legitimately occur more than once in a type.
+                visited.remove(var);
+            }
+        }
+        if (type instanceof ParameterizedType) {
+            final ParameterizedType p = (ParameterizedType) type;
+            if (hasCyclicOwnerChain(p) || !unrolling.add(p)) {
+                return p;
+            }
+            try {
+                final Type[] args = p.getActualTypeArguments();
+                final Type[] unrolledArgs = args.clone();
+                boolean changed = false;
                 for (int i = 0; i < args.length; i++) {
-                    final Type unrolled = 
unrollVariables(parameterizedTypeArguments, args[i], visited);
-                    if (unrolled != null) {
-                        args[i] = unrolled;
+                    final Type unrolled = unrollVariables(typeArguments, 
args[i], visited, unrolling);
+                    if (unrolled != null && unrolled != args[i]) {
+                        unrolledArgs[i] = unrolled;
+                        changed = true;
                     }
                 }
-                return parameterizeWithOwner(p.getOwnerType(), (Class<?>) 
p.getRawType(), args);
+                final Type owner = p.getOwnerType();
+                final Type unrolledOwner = owner != null && !(owner instanceof 
Class<?>) ? unrollVariables(typeArguments, owner, visited, unrolling) : owner;
+                if (!changed && unrolledOwner == owner) {
+                    return p;
+                }
+                return parameterizeWithOwner(unrolledOwner, (Class<?>) 
p.getRawType(), unrolledArgs);
+            } finally {
+                unrolling.remove(p);
             }
-            if (type instanceof WildcardType) {
-                final WildcardType wild = (WildcardType) type;
-                return 
wildcardType().withUpperBounds(unrollBounds(typeArguments, 
wild.getUpperBounds()))
-                        .withLowerBounds(unrollBounds(typeArguments, 
wild.getLowerBounds())).build();
+        }
+        if (type instanceof WildcardType) {
+            final WildcardType wild = (WildcardType) type;
+            if (!unrolling.add(wild)) {
+                return wild;
+            }
+            try {
+                final Type[] upperBounds = wild.getUpperBounds();
+                final Type[] lowerBounds = wild.getLowerBounds();
+                final Type[] unrolledUpperBounds = unrollBounds(typeArguments, 
upperBounds.clone(), visited, unrolling);
+                final Type[] unrolledLowerBounds = unrollBounds(typeArguments, 
lowerBounds.clone(), visited, unrolling);
+                if (isSameTypes(upperBounds, unrolledUpperBounds) && 
isSameTypes(lowerBounds, unrolledLowerBounds)) {
+                    return wild;
+                }
+                return 
wildcardType().withUpperBounds(unrolledUpperBounds).withLowerBounds(unrolledLowerBounds).build();
+            } finally {
+                unrolling.remove(wild);
             }
         }
         return type;
     }
 
+    /**
+     * Tests whether two type arrays hold the very same types, compared by 
identity so that no {@code equals()} or {@code hashCode()} of a custom {@link 
Type}
+     * is called.
+     *
+     * @param types      The first array.
+     * @param otherTypes The second array.
+     * @return Whether both arrays have the same length and the same types at 
each index.
+     */
+    private static boolean isSameTypes(final Type[] types, final Type[] 
otherTypes) {
+        if (types.length != otherTypes.length) {
+            return false;
+        }
+        for (int i = 0; i < types.length; i++) {
+            if (types[i] != otherTypes[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Creates a new {@link WildcardTypeBuilder}.
      *
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 8b0749c34..f0e0e9d52 100644
--- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
@@ -24,6 +24,7 @@
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -47,6 +48,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Properties;
 import java.util.Set;
 import java.util.TreeSet;
@@ -266,6 +268,83 @@ public class Other<T> implements This<String, T> {
         // empty
     }
 
+    /**
+     * A custom {@link ParameterizedType} with a structural {@code 
hashCode()}, whose owner and type arguments can be set after construction so 
that tests can
+     * create cycles.
+     */
+    private static final class CyclicParameterizedType implements 
ParameterizedType {
+
+        private final Class<?> rawType;
+        private Type[] typeArguments;
+        private Type ownerType;
+
+        CyclicParameterizedType(final Class<?> rawType, final Type... 
typeArguments) {
+            this.rawType = rawType;
+            this.typeArguments = typeArguments;
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            return this == obj;
+        }
+
+        @Override
+        public Type[] getActualTypeArguments() {
+            return typeArguments.clone();
+        }
+
+        @Override
+        public Type getOwnerType() {
+            return ownerType;
+        }
+
+        @Override
+        public Type getRawType() {
+            return rawType;
+        }
+
+        @Override
+        public String toString() {
+            return "CyclicParameterizedType<" + rawType.getName() + ">";
+        }
+
+        @Override
+        public int hashCode() {
+            // Structural like the JDK implementation: it follows the owner, 
so a cycle would overflow the stack.
+            return Objects.hash(rawType, Arrays.hashCode(typeArguments), 
ownerType);
+        }
+
+        void setOwnerType(final Type ownerType) {
+            this.ownerType = ownerType;
+        }
+
+        void setTypeArguments(final Type... typeArguments) {
+            this.typeArguments = typeArguments;
+        }
+    }
+
+    /** LANG-1836: non-static inner classes whose owner type is parameterized. 
*/
+    static class Outer<T> {
+
+        class Inner {
+            // empty
+        }
+
+        class InnerU<U> {
+            // empty
+        }
+
+        Map.Entry<T, Integer> entry;
+
+        Inner inner;
+
+        InnerU<Integer> innerUInteger;
+
+        InnerU<T> innerUT;
+
+        Map<T, T> map;
+    }
+
     public class Tester implements This<String, B> {
         // empty
     }
@@ -497,6 +576,18 @@ void testContainsTypeVariablesMultiBoundWildcard() {
         assertFalse(TypeUtils.containsTypeVariables(wtNone));
     }
 
+    @Test
+    void testContainsTypeVariablesOwnerType() throws NoSuchFieldException {
+        // LANG-1836: Outer<T>.Inner
+        
assertTrue(TypeUtils.containsTypeVariables(Outer.class.getDeclaredField("inner").getGenericType()));
+        // Outer<T>.InnerU<Integer>
+        
assertTrue(TypeUtils.containsTypeVariables(Outer.class.getDeclaredField("innerUInteger").getGenericType()));
+        // Outer<String>.Inner
+        
assertFalse(TypeUtils.containsTypeVariables(TypeUtils.parameterizeWithOwner(TypeUtils.parameterize(Outer.class,
 String.class), Outer.Inner.class)));
+        // Map.Entry<String, Integer>: the owner is the raw Map.class, which 
declares type parameters but binds none.
+        
assertFalse(TypeUtils.containsTypeVariables(TypeUtils.parameterizeWithOwner(Map.class,
 Map.Entry.class, String.class, Integer.class)));
+    }
+
     @Test
     void testContainsTypeVariablesPr437() throws Exception {
         abstract class Test2<G> {
@@ -1589,6 +1680,176 @@ void testUnrollVariables() {
         assertEquals("java.util.ArrayList<java.lang.String>", 
TypeUtils.unrollVariables(mapping, parameterizedType).getTypeName());
     }
 
+    @Test
+    void testUnrollVariablesOwnerType() throws NoSuchFieldException {
+        // LANG-1836
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final Map<TypeVariable<?>, Type> mapping = 
Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
+        final ParameterizedType outerString = 
TypeUtils.parameterize(Outer.class, String.class);
+        // Outer<T>.Inner -> Outer<String>.Inner
+        assertEquals(TypeUtils.parameterizeWithOwner(outerString, 
Outer.Inner.class),
+                TypeUtils.unrollVariables(mapping, 
Outer.class.getDeclaredField("inner").getGenericType()));
+        // Outer<T>.InnerU<Integer> -> Outer<String>.InnerU<Integer>
+        assertEquals(TypeUtils.parameterizeWithOwner(outerString, 
Outer.InnerU.class, Integer.class),
+                TypeUtils.unrollVariables(mapping, 
Outer.class.getDeclaredField("innerUInteger").getGenericType()));
+        // Outer<T>.InnerU<T> -> Outer<String>.InnerU<String>
+        assertEquals(TypeUtils.parameterizeWithOwner(outerString, 
Outer.InnerU.class, String.class),
+                TypeUtils.unrollVariables(mapping, 
Outer.class.getDeclaredField("innerUT").getGenericType()));
+        // Map.Entry<T, Integer> -> Map.Entry<String, Integer>: the raw owner 
Map.class is left unchanged.
+        assertEquals(TypeUtils.parameterizeWithOwner(Map.class, 
Map.Entry.class, String.class, Integer.class),
+                TypeUtils.unrollVariables(mapping, 
Outer.class.getDeclaredField("entry").getGenericType()));
+    }
+
+    @Test
+    void testContainsTypeVariablesSelfOwnedTypeWithStructuralHashCode() {
+        // LANG-1836: detecting the cycle must not call hashCode(), which 
would follow the owner again.
+        final CyclicParameterizedType self = new 
CyclicParameterizedType(Outer.Inner.class);
+        self.setOwnerType(self);
+        assertFalse(TypeUtils.containsTypeVariables(self));
+    }
+
+    @Test
+    void testContainsTypeVariablesCyclicOwnerType() {
+        // LANG-1836: a (custom) ParameterizedType that is its own owner must 
not cause a StackOverflowError.
+        final ParameterizedType[] self = new ParameterizedType[1];
+        self[0] = new ParameterizedType() {
+
+            @Override
+            public Type[] getActualTypeArguments() {
+                return new Type[0];
+            }
+
+            @Override
+            public Type getOwnerType() {
+                return self[0];
+            }
+
+            @Override
+            public Type getRawType() {
+                return Outer.Inner.class;
+            }
+        };
+        assertFalse(TypeUtils.containsTypeVariables(self[0]));
+    }
+
+    @Test
+    void testContainsTypeVariablesSelfReferencingTypeArgument() {
+        // LANG-1836: a (custom) ParameterizedType that is its own type 
argument must not cause a StackOverflowError.
+        final CyclicParameterizedType self = new 
CyclicParameterizedType(List.class);
+        self.setTypeArguments(self);
+        assertFalse(TypeUtils.containsTypeVariables(self));
+    }
+
+    @Test
+    void testContainsTypeVariablesMutuallyReferencingTypeArguments() {
+        // LANG-1836
+        final CyclicParameterizedType a = new 
CyclicParameterizedType(List.class);
+        final CyclicParameterizedType b = new 
CyclicParameterizedType(List.class);
+        a.setTypeArguments(b);
+        b.setTypeArguments(a);
+        assertFalse(TypeUtils.containsTypeVariables(a));
+        assertFalse(TypeUtils.containsTypeVariables(b));
+    }
+
+    @Test
+    void testContainsTypeVariablesSelfReferencingTypeArgumentBesideVariable() {
+        // LANG-1836: cutting the cycle must not hide a variable that is found 
elsewhere.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType self = new 
CyclicParameterizedType(Map.class);
+        self.setTypeArguments(self, t);
+        assertTrue(TypeUtils.containsTypeVariables(self));
+    }
+
+    @Test
+    void testUnrollVariablesWildcardCycleThroughOwnerType() {
+        // LANG-1836: {T -> Outer<? extends T>.Inner} must not cause a 
StackOverflowError when T is unrolled.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final WildcardType extendsT = 
TypeUtils.wildcardType().withUpperBounds(t).build();
+        final Type outerExtendsT = 
TypeUtils.parameterizeWithOwner(TypeUtils.parameterize(Outer.class, extendsT), 
Outer.Inner.class);
+        final Map<TypeVariable<?>, Type> mapping = 
Collections.<TypeVariable<?>, Type>singletonMap(t, outerExtendsT);
+        assertEquals(outerExtendsT, TypeUtils.unrollVariables(mapping, t));
+    }
+
+    @Test
+    void testUnrollVariablesSelfReferencingTypeArgument() {
+        // LANG-1836: the cycle through the type argument is left as it is 
while the variable is still unrolled.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType self = new 
CyclicParameterizedType(Map.class);
+        self.setTypeArguments(self, t);
+        final Map<TypeVariable<?>, Type> mapping = 
Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
+        final Type[] unrolledArguments = ((ParameterizedType) 
TypeUtils.unrollVariables(mapping, self)).getActualTypeArguments();
+        assertSame(self, unrolledArguments[0]);
+        assertEquals(String.class, unrolledArguments[1]);
+    }
+
+    @Test
+    void testUnrollVariablesCyclicOwnerChain() {
+        // LANG-1836: A -> owner B<T> -> owner A is left as it is and must not 
cause a StackOverflowError.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType a = new 
CyclicParameterizedType(Outer.Inner.class);
+        final CyclicParameterizedType b = new 
CyclicParameterizedType(Outer.class, t);
+        a.setOwnerType(b);
+        b.setOwnerType(a);
+        final Map<TypeVariable<?>, Type> mapping = 
Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
+        final ParameterizedType unrolled = (ParameterizedType) 
TypeUtils.unrollVariables(mapping, a);
+        assertEquals(Outer.Inner.class, unrolled.getRawType());
+        assertSame(b, unrolled.getOwnerType());
+    }
+
+    @Test
+    void testUnrollVariablesCyclicOwnerChainFromEitherType() {
+        // LANG-1836: the cycle is left alone whichever type of the cycle is 
unrolled first.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType a = new 
CyclicParameterizedType(Outer.Inner.class);
+        final CyclicParameterizedType b = new 
CyclicParameterizedType(Outer.class, t);
+        a.setOwnerType(b);
+        b.setOwnerType(a);
+        final Map<TypeVariable<?>, Type> mapping = 
Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
+        assertSame(b, TypeUtils.unrollVariables(mapping, b));
+    }
+
+    @Test
+    void testUnrollVariablesSelfOwnedTypeWithTypeVariable() {
+        // LANG-1836
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType self = new 
CyclicParameterizedType(Outer.InnerU.class, t);
+        self.setOwnerType(self);
+        final Map<TypeVariable<?>, Type> mapping = 
Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
+        assertSame(self, TypeUtils.unrollVariables(mapping, self));
+    }
+
+    @Test
+    void testUnrollVariablesCycleThroughTypeArgumentAndOwner() {
+        // LANG-1836: Map<Inner, T> whose argument Inner has the Map as its 
owner. The cycle is left as it is instead of overflowing the stack, while T is 
unrolled.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType inner = new 
CyclicParameterizedType(Outer.Inner.class);
+        final CyclicParameterizedType map = new 
CyclicParameterizedType(Map.class, inner, t);
+        inner.setOwnerType(map);
+        final Map<TypeVariable<?>, Type> mapping = 
Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
+        final Type[] unrolledArguments = ((ParameterizedType) 
TypeUtils.unrollVariables(mapping, map)).getActualTypeArguments();
+        assertSame(inner, unrolledArguments[0]);
+        assertEquals(String.class, unrolledArguments[1]);
+    }
+
+    @Test
+    void testUnrollVariablesReturnsTheSameTypeWhenNothingChanges() {
+        // LANG-1836: a type that unrolling does not change is returned as it 
is instead of being rebuilt.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final Type listOfT = TypeUtils.parameterize(List.class, t);
+        assertSame(listOfT, TypeUtils.unrollVariables(Collections.emptyMap(), 
listOfT));
+        final Type listOfString = TypeUtils.parameterize(List.class, 
String.class);
+        assertSame(listOfString, 
TypeUtils.unrollVariables(Collections.emptyMap(), listOfString));
+    }
+
+    @Test
+    void testUnrollVariablesRepeatedVariable() throws NoSuchFieldException {
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final Map<TypeVariable<?>, Type> mapping = 
Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
+        // Map<T, T> -> Map<String, String>: every occurrence of T is 
unrolled, not only the first.
+        assertEquals(TypeUtils.parameterize(Map.class, String.class, 
String.class),
+                TypeUtils.unrollVariables(mapping, 
Outer.class.getDeclaredField("map").getGenericType()));
+    }
+
     @SuppressWarnings("unlikely-arg-type")
     @Test
     void testWildcardType() throws NoSuchFieldException {

Reply via email to