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 e1ce120c6 Fix TypeUtils.toString() recursion and bound handling on 
recursive generic types (#1789)
e1ce120c6 is described below

commit e1ce120c6a60df8fe1bf37b78127eea7562662e4
Author: gaurav kumar pandey <[email protected]>
AuthorDate: Mon Sep 14 03:54:31 2026 +0530

    Fix TypeUtils.toString() recursion and bound handling on recursive generic 
types (#1789)
    
    * Fix TypeUtils.toString() recursion and bound handling on recursive 
generic types
    
    - Format embedded TypeVariables as type references (name only) in 
ParameterizedType type arguments, WildcardType bounds, and GenericArrayType 
component types, preventing recursion and StackOverflowError
    - Preserve valid interface bounds on TypeVariables instead of stripping them
    - Remove fragile heuristic methods findRecursiveTypes, 
appendRecursiveTypes, and containsVariableTypeSameParametrizedTypeBound that 
corrupted formatting to <T><U><U>
    - Add recursion guard with VISITING ThreadLocal cleanup on unwind
    - Fix TypeUtils.containsTypeVariables(WildcardType) to check all upper and 
lower bounds instead of only index 0
    
    * Cover all Type nodes with identity-based recursion guard and defensively 
copy bounds in WildcardTypeImpl
    
    * Add tests and safeguards for review feedback on TypeUtils formatting
    
    - Add tests for bounded T[], List<T>, and <T extends Number, S extends T> 
formatting
    - Add tests for bounded TypeVariable passed to toLongString()
    - Add test for parameterized owner with non-generic inner class exercising 
removal of <>
    - Add test for defensive copying of lower bounds in WildcardTypeImpl
    - Add tests for owner cycles, repeated sibling references, and ThreadLocal 
cleanup after exception
    - Route non-Class owner types and toLongString TypeVariables through 
toString(Type) for cycle tracking
---
 .../apache/commons/lang3/reflect/TypeUtils.java    | 160 ++++++----
 .../commons/lang3/reflect/TypeUtilsTest.java       | 332 +++++++++++++++++++++
 2 files changed, 427 insertions(+), 65 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 92dfff7da..4c8491efd 100644
--- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
+++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
@@ -28,6 +28,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -235,8 +236,8 @@ private static final class WildcardTypeImpl implements 
WildcardType {
          * @param lowerBounds of this type.
          */
         private WildcardTypeImpl(final Type[] upperBounds, final Type[] 
lowerBounds) {
-            this.upperBounds = ObjectUtils.getIfNull(upperBounds, 
ArrayUtils.EMPTY_TYPE_ARRAY);
-            this.lowerBounds = ObjectUtils.getIfNull(lowerBounds, 
ArrayUtils.EMPTY_TYPE_ARRAY);
+            this.upperBounds = upperBounds != null ? upperBounds.clone() : 
ArrayUtils.EMPTY_TYPE_ARRAY;
+            this.lowerBounds = lowerBounds != null ? lowerBounds.clone() : 
ArrayUtils.EMPTY_TYPE_ARRAY;
         }
 
         /**
@@ -290,7 +291,7 @@ public String toString() {
     // @formatter:off
     private static final AppendableJoiner<Type> AMP_JOINER = 
AppendableJoiner.<Type>builder()
             .setDelimiter(" & ")
-            .setElementAppender((a, e) -> a.append(toString(e)))
+            .setElementAppender((a, e) -> a.append(toReferenceString(e)))
             .get();
     // @formatter:on
 
@@ -316,6 +317,18 @@ public String toString() {
             .get();
     // @formatter:on
 
+    /**
+     * Type arguments joiner.
+     */
+    // @formatter:off
+    private static final AppendableJoiner<Type> TYPE_ARG_JOINER = 
AppendableJoiner.<Type>builder()
+            .setPrefix("<")
+            .setSuffix(">")
+            .setDelimiter(", ")
+            .setElementAppender((a, e) -> a.append(toReferenceString(e)))
+            .get();
+    // @formatter:on
+
     /**
      * A wildcard instance matching {@code ?}.
      *
@@ -327,17 +340,20 @@ private static <T> String anyToString(final T object) {
         return object instanceof Type ? toString((Type) object) : 
object.toString();
     }
 
-    private static void appendRecursiveTypes(final StringBuilder builder, 
final int[] recursiveTypeIndexes, final Type[] argumentTypes) {
-        for (final Type type : argumentTypes) {
-            // toString() or you get a SO
-            GT_JOINER.join(builder, Objects.toString(type));
-        }
-        final Type[] argumentsFiltered = ArrayUtils.removeAll(argumentTypes, 
recursiveTypeIndexes);
-        if (argumentsFiltered.length > 0) {
-            GT_JOINER.join(builder, (Object[]) argumentsFiltered);
+    /**
+     * Formats a {@link Type} as a type reference string (type variables are 
formatted by name only without bounds).
+     *
+     * @param type The type to format.
+     * @return String.
+     */
+    private static String toReferenceString(final Type type) {
+        if (type instanceof TypeVariable<?>) {
+            return ((TypeVariable<?>) type).getName();
         }
+        return toString(type);
     }
 
+
     /**
      * Formats a {@link Class} as a {@link String}.
      *
@@ -387,7 +403,17 @@ public static boolean containsTypeVariables(final Type 
type) {
         }
         if (type instanceof WildcardType) {
             final WildcardType wild = (WildcardType) type;
-            return containsTypeVariables(getImplicitLowerBounds(wild)[0]) || 
containsTypeVariables(getImplicitUpperBounds(wild)[0]);
+            for (final Type bound : getImplicitLowerBounds(wild)) {
+                if (containsTypeVariables(bound)) {
+                    return true;
+                }
+            }
+            for (final Type bound : getImplicitUpperBounds(wild)) {
+                if (containsTypeVariables(bound)) {
+                    return true;
+                }
+            }
+            return false;
         }
         if (type instanceof GenericArrayType) {
             return containsTypeVariables(((GenericArrayType) 
type).getGenericComponentType());
@@ -395,10 +421,6 @@ public static boolean containsTypeVariables(final Type 
type) {
         return false;
     }
 
-    private static boolean containsVariableTypeSameParametrizedTypeBound(final 
TypeVariable<?> typeVariable, final ParameterizedType parameterizedType) {
-        return ArrayUtils.contains(typeVariable.getBounds(), 
parameterizedType);
-    }
-
     /**
      * Tries to determine the type arguments of a class/interface based on a 
super parameterized type's type arguments. This method is the inverse of
      * {@link #getTypeArguments(Type, Class)} which gets a class/interface's 
type arguments based on a subtype. It is far more limited in determining the 
type
@@ -551,17 +573,6 @@ private static Type[] extractTypeArgumentsFrom(final 
Map<TypeVariable<?>, Type>
         return result;
     }
 
-    private static int[] findRecursiveTypes(final ParameterizedType 
parameterizedType) {
-        final Type[] filteredArgumentTypes = 
Arrays.copyOf(parameterizedType.getActualTypeArguments(), 
parameterizedType.getActualTypeArguments().length);
-        int[] indexesToRemove = {};
-        for (int i = 0; i < filteredArgumentTypes.length; i++) {
-            if (filteredArgumentTypes[i] instanceof TypeVariable<?>
-                    && 
containsVariableTypeSameParametrizedTypeBound((TypeVariable<?>) 
filteredArgumentTypes[i], parameterizedType)) {
-                indexesToRemove = ArrayUtils.add(indexesToRemove, i);
-            }
-        }
-        return indexesToRemove;
-    }
 
     /**
      * Creates a generic array type instance.
@@ -581,7 +592,7 @@ public static GenericArrayType genericArrayType(final Type 
componentType) {
      * @return String.
      */
     private static String genericArrayTypeToString(final GenericArrayType 
genericArrayType) {
-        return String.format("%s[]", 
toString(genericArrayType.getGenericComponentType()));
+        return String.format("%s[]", 
toReferenceString(genericArrayType.getGenericComponentType()));
     }
 
     /**
@@ -1446,15 +1457,13 @@ private static String parameterizedTypeToString(final 
ParameterizedType paramete
             if (useOwner instanceof Class<?>) {
                 builder.append(((Class<?>) useOwner).getName());
             } else {
-                builder.append(useOwner);
+                builder.append(toString(useOwner));
             }
             builder.append('.').append(raw.getSimpleName());
         }
-        final int[] recursiveTypeIndexes = 
findRecursiveTypes(parameterizedType);
-        if (recursiveTypeIndexes.length > 0) {
-            appendRecursiveTypes(builder, recursiveTypeIndexes, 
parameterizedType.getActualTypeArguments());
-        } else {
-            GT_JOINER.join(builder, (Object[]) 
parameterizedType.getActualTypeArguments());
+        final Type[] typeArguments = 
parameterizedType.getActualTypeArguments();
+        if (typeArguments.length > 0) {
+            TYPE_ARG_JOINER.join(builder, typeArguments);
         }
         return builder.toString();
     }
@@ -1549,7 +1558,31 @@ public static String toLongString(final TypeVariable<?> 
typeVariable) {
         } else {
             buf.append(d);
         }
-        return 
buf.append(':').append(typeVariableToString(typeVariable)).toString();
+        return buf.append(':').append(toString(typeVariable)).toString();
+    }
+
+    private static final ThreadLocal<Set<Type>> VISITING = 
ThreadLocal.withInitial(() -> Collections.newSetFromMap(new 
IdentityHashMap<>()));
+
+    private static String toCyclicString(final Type type) {
+        if (type instanceof Class<?>) {
+            return ((Class<?>) type).getSimpleName() + "(cycle)";
+        }
+        if (type instanceof TypeVariable<?>) {
+            return ((TypeVariable<?>) type).getName() + "(cycle)";
+        }
+        if (type instanceof WildcardType) {
+            return "? (cycle)";
+        }
+        if (type instanceof ParameterizedType) {
+            final ParameterizedType pt = (ParameterizedType) type;
+            final Type raw = pt.getRawType();
+            final String rawName = raw instanceof Class<?> ? ((Class<?>) 
raw).getSimpleName() : raw.getTypeName();
+            return rawName + "(cycle)";
+        }
+        if (type instanceof GenericArrayType) {
+            return "(cycle)";
+        }
+        return ObjectUtils.identityToString(type) + "(cycle)";
     }
 
     /**
@@ -1562,22 +1595,33 @@ public static String toLongString(final TypeVariable<?> 
typeVariable) {
      */
     public static String toString(final Type type) {
         Objects.requireNonNull(type, "type");
-        if (type instanceof Class<?>) {
-            return classToString((Class<?>) type);
-        }
-        if (type instanceof ParameterizedType) {
-            return parameterizedTypeToString((ParameterizedType) type);
-        }
-        if (type instanceof WildcardType) {
-            return wildcardTypeToString((WildcardType) type);
+        final Set<Type> visiting = VISITING.get();
+        if (!visiting.add(type)) {
+            return toCyclicString(type);
         }
-        if (type instanceof TypeVariable<?>) {
-            return typeVariableToString((TypeVariable<?>) type);
-        }
-        if (type instanceof GenericArrayType) {
-            return genericArrayTypeToString((GenericArrayType) type);
+        try {
+            if (type instanceof Class<?>) {
+                return classToString((Class<?>) type);
+            }
+            if (type instanceof ParameterizedType) {
+                return parameterizedTypeToString((ParameterizedType) type);
+            }
+            if (type instanceof WildcardType) {
+                return wildcardTypeToString((WildcardType) type);
+            }
+            if (type instanceof TypeVariable<?>) {
+                return typeVariableToString((TypeVariable<?>) type);
+            }
+            if (type instanceof GenericArrayType) {
+                return genericArrayTypeToString((GenericArrayType) type);
+            }
+            throw new 
IllegalArgumentException(ObjectUtils.identityToString(type));
+        } finally {
+            visiting.remove(type);
+            if (visiting.isEmpty()) {
+                VISITING.remove();
+            }
         }
-        throw new IllegalArgumentException(ObjectUtils.identityToString(type));
     }
 
     /**
@@ -1615,22 +1659,8 @@ private static String typeVariableToString(final 
TypeVariable<?> typeVariable) {
         final StringBuilder builder = new 
StringBuilder(typeVariable.getName());
         final Type[] bounds = typeVariable.getBounds();
         if (bounds.length > 0 && !(bounds.length == 1 && 
Object.class.equals(bounds[0]))) {
-            // https://issues.apache.org/jira/projects/LANG/issues/LANG-1698
-            // There must be a better way to avoid a stack overflow on Java 17 
and up.
-            // Bounds are different in Java 17 and up where instead of Object 
you can get an interface like Comparable.
-            final Type bound = bounds[0];
-            boolean append = true;
-            if (bound instanceof ParameterizedType) {
-                final Type rawType = ((ParameterizedType) bound).getRawType();
-                if (rawType instanceof Class && ((Class<?>) 
rawType).isInterface()) {
-                    // Avoid recursion and stack overflow on Java 17 and up.
-                    append = false;
-                }
-            }
-            if (append) {
-                builder.append(" extends ");
-                AMP_JOINER.join(builder, bounds);
-            }
+            builder.append(" extends ");
+            AMP_JOINER.join(builder, bounds);
         }
         return builder.toString();
     }
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 5dc6d15f9..6c00f8204 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.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.awt.Insets;
@@ -47,10 +48,13 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.TreeSet;
+import java.util.function.BiFunction;
 import java.util.stream.Stream;
 
 import org.apache.commons.lang3.AbstractLangTest;
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.reflect.testbed.Foo;
 import org.apache.commons.lang3.reflect.testbed.GenericParent;
 import org.apache.commons.lang3.reflect.testbed.GenericTypeHolder;
@@ -171,6 +175,40 @@ abstract class Test1<G> {
     public abstract <K, V> Map<? extends K, ? super V[]> m9();
 }
 
+class MySuperClass<T> {
+    // empty
+}
+
+class MyClass<U extends MySuperClass<? super U>> {
+    // empty
+}
+
+class MultiBoundClass<U extends Number & Comparable<? super U>> {
+    // empty
+}
+
+class TwoParams<T extends TwoParams<T, U>, U> {
+    // empty
+}
+
+class InterfaceBound<T extends List<String>> {
+    // empty
+}
+
+class DependentBounds<T extends Number, S extends T> {
+    // empty
+}
+
+class ParameterizedOwner<T> {
+    class NonGenericInner {
+        // empty
+    }
+
+    public NonGenericInner getInner() {
+        return null;
+    }
+}
+
 /**
  * Tests {@link TypeUtils}.
  *
@@ -1296,4 +1334,298 @@ void testWrap() {
         assertEquals(String.class, TypeUtils.wrap(String.class).getType());
     }
 
+    @Test
+    void testRecursiveTypeWildcardBoundClass() {
+        assertEquals("org.apache.commons.lang3.reflect.MyClass<U extends 
org.apache.commons.lang3.reflect.MySuperClass<? super U>>",
+                TypeUtils.toString(MyClass.class));
+        assertEquals("U extends 
org.apache.commons.lang3.reflect.MySuperClass<? super U>",
+                TypeUtils.toString(MyClass.class.getTypeParameters()[0]));
+    }
+
+    @Test
+    void testMultiBoundRecursiveType() {
+        assertEquals("org.apache.commons.lang3.reflect.MultiBoundClass<U 
extends java.lang.Number & java.lang.Comparable<? super U>>",
+                TypeUtils.toString(MultiBoundClass.class));
+        assertEquals("U extends java.lang.Number & java.lang.Comparable<? 
super U>",
+                
TypeUtils.toString(MultiBoundClass.class.getTypeParameters()[0]));
+    }
+
+    @Test
+    void testInterfaceBoundPreserved() {
+        assertEquals("T extends java.util.List<java.lang.String>",
+                
TypeUtils.toString(InterfaceBound.class.getTypeParameters()[0]));
+    }
+
+    @Test
+    void testMultiParamRecursiveType() {
+        final ParameterizedType parameterizedType = 
TypeUtils.parameterize(TwoParams.class, TwoParams.class.getTypeParameters());
+        assertEquals("org.apache.commons.lang3.reflect.TwoParams<T, U>",
+                TypeUtils.toString(parameterizedType));
+    }
+
+    @Test
+    void testGClassToString() {
+        assertEquals("org.apache.commons.lang3.reflect.AClass.GClass<T extends 
org.apache.commons.lang3.reflect.AClass.BClass<? extends T> "
+                + "& 
org.apache.commons.lang3.reflect.AClass.AInterface<org.apache.commons.lang3.reflect.AClass.AInterface<?
 super T>>>",
+                TypeUtils.toString(AClass.GClass.class));
+    }
+
+    @Test
+    void testContainsTypeVariablesMultiBoundWildcard() {
+        final TypeVariable<?> t = getClass().getTypeParameters()[0];
+        final WildcardType wtUpper = 
TypeUtils.wildcardType().withUpperBounds(Integer.class, t).build();
+        assertTrue(TypeUtils.containsTypeVariables(wtUpper));
+        final WildcardType wtLower = 
TypeUtils.wildcardType().withLowerBounds(Integer.class, t).build();
+        assertTrue(TypeUtils.containsTypeVariables(wtLower));
+        final WildcardType wtNone = 
TypeUtils.wildcardType().withUpperBounds(Integer.class, String.class).build();
+        assertFalse(TypeUtils.containsTypeVariables(wtNone));
+    }
+
+    @Test
+    void testWildcardTypeBuilderDefensiveCopy() {
+        // Upper bounds defensive copying on input array and getter
+        final Type[] upperBounds = { String.class };
+        final WildcardType wildcardUpper = 
TypeUtils.wildcardType().withUpperBounds(upperBounds).build();
+        upperBounds[0] = Integer.class;
+        assertArrayEquals(new Type[] { String.class }, 
wildcardUpper.getUpperBounds());
+        wildcardUpper.getUpperBounds()[0] = Integer.class;
+        assertArrayEquals(new Type[] { String.class }, 
wildcardUpper.getUpperBounds());
+
+        // Lower bounds defensive copying on input array and getter
+        final Type[] lowerBounds = { String.class };
+        final WildcardType wildcardLower = 
TypeUtils.wildcardType().withLowerBounds(lowerBounds).build();
+        lowerBounds[0] = Integer.class;
+        assertArrayEquals(new Type[] { String.class }, 
wildcardLower.getLowerBounds());
+        wildcardLower.getLowerBounds()[0] = Integer.class;
+        assertArrayEquals(new Type[] { String.class }, 
wildcardLower.getLowerBounds());
+    }
+
+    @Test
+    void testBoundedGenericArrayTypeToString() {
+        final TypeVariable<?> t = DependentBounds.class.getTypeParameters()[0];
+        final GenericArrayType gat = TypeUtils.genericArrayType(t);
+        assertEquals("T[]", TypeUtils.toString(gat));
+    }
+
+    @Test
+    void testBoundedParameterizedTypeArgumentToString() {
+        final TypeVariable<?> t = DependentBounds.class.getTypeParameters()[0];
+        final ParameterizedType pt = TypeUtils.parameterize(List.class, t);
+        assertEquals("java.util.List<T>", TypeUtils.toString(pt));
+    }
+
+    @Test
+    void testDependentBoundsClassAndTypeParametersToString() {
+        assertEquals("org.apache.commons.lang3.reflect.DependentBounds<T 
extends java.lang.Number, S extends T>",
+                TypeUtils.toString(DependentBounds.class));
+        assertEquals("T extends java.lang.Number",
+                
TypeUtils.toString(DependentBounds.class.getTypeParameters()[0]));
+        assertEquals("S extends T",
+                
TypeUtils.toString(DependentBounds.class.getTypeParameters()[1]));
+    }
+
+    @Test
+    void testToLongStringBoundedTypeVariable() {
+        assertEquals("org.apache.commons.lang3.reflect.DependentBounds:T 
extends java.lang.Number",
+                
TypeUtils.toLongString(DependentBounds.class.getTypeParameters()[0]));
+        assertEquals("org.apache.commons.lang3.reflect.DependentBounds:S 
extends T",
+                
TypeUtils.toLongString(DependentBounds.class.getTypeParameters()[1]));
+        assertEquals("org.apache.commons.lang3.reflect.MultiBoundClass:U 
extends java.lang.Number & java.lang.Comparable<? super U>",
+                
TypeUtils.toLongString(MultiBoundClass.class.getTypeParameters()[0]));
+        assertEquals("org.apache.commons.lang3.reflect.InterfaceBound:T 
extends java.util.List<java.lang.String>",
+                
TypeUtils.toLongString(InterfaceBound.class.getTypeParameters()[0]));
+        assertEquals("org.apache.commons.lang3.reflect.MyClass:U extends 
org.apache.commons.lang3.reflect.MySuperClass<? super U>",
+                TypeUtils.toLongString(MyClass.class.getTypeParameters()[0]));
+    }
+
+    @Test
+    void testParameterizedOwnerWithNonGenericInnerClassToString() throws 
NoSuchMethodException {
+        final ParameterizedType owner = 
TypeUtils.parameterize(ParameterizedOwner.class, String.class);
+        final ParameterizedType nonGenericInner = 
TypeUtils.parameterizeWithOwner(owner, 
ParameterizedOwner.NonGenericInner.class);
+        
assertEquals("org.apache.commons.lang3.reflect.ParameterizedOwner<java.lang.String>.NonGenericInner",
+                TypeUtils.toString(nonGenericInner));
+
+        final Type methodReturnType = 
ParameterizedOwner.class.getMethod("getInner").getGenericReturnType();
+        
assertEquals("org.apache.commons.lang3.reflect.ParameterizedOwner<T>.NonGenericInner",
+                TypeUtils.toString(methodReturnType));
+    }
+
+    @Test
+    void testCyclicOwnerParameterizedTypeToString() {
+        final ParameterizedType[] holder = new ParameterizedType[1];
+        final ParameterizedType cyclicOwnerType = new ParameterizedType() {
+            @Override
+            public Type[] getActualTypeArguments() {
+                return ArrayUtils.EMPTY_TYPE_ARRAY;
+            }
+
+            @Override
+            public Type getRawType() {
+                return List.class;
+            }
+
+            @Override
+            public Type getOwnerType() {
+                return holder[0];
+            }
+        };
+        holder[0] = cyclicOwnerType;
+        assertEquals("List(cycle).List", TypeUtils.toString(cyclicOwnerType));
+
+        final ParameterizedType[] holderA = new ParameterizedType[1];
+        final ParameterizedType[] holderB = new ParameterizedType[1];
+        final ParameterizedType typeA = new ParameterizedType() {
+            @Override
+            public Type[] getActualTypeArguments() {
+                return ArrayUtils.EMPTY_TYPE_ARRAY;
+            }
+
+            @Override
+            public Type getRawType() {
+                return Map.class;
+            }
+
+            @Override
+            public Type getOwnerType() {
+                return holderB[0];
+            }
+        };
+        final ParameterizedType typeB = new ParameterizedType() {
+            @Override
+            public Type[] getActualTypeArguments() {
+                return ArrayUtils.EMPTY_TYPE_ARRAY;
+            }
+
+            @Override
+            public Type getRawType() {
+                return Set.class;
+            }
+
+            @Override
+            public Type getOwnerType() {
+                return holderA[0];
+            }
+        };
+        holderA[0] = typeA;
+        holderB[0] = typeB;
+        assertEquals("Map(cycle).Set.Map", TypeUtils.toString(typeA));
+    }
+
+    @Test
+    void testRepeatedSiblingReferencesToString() {
+        final ParameterizedType listString = 
TypeUtils.parameterize(List.class, String.class);
+        final ParameterizedType mapType = TypeUtils.parameterize(Map.class, 
listString, listString);
+        assertEquals("java.util.Map<java.util.List<java.lang.String>, 
java.util.List<java.lang.String>>",
+                TypeUtils.toString(mapType));
+
+        final TypeVariable<?> t = DependentBounds.class.getTypeParameters()[0];
+        final ParameterizedType biFunctionType = 
TypeUtils.parameterize(BiFunction.class, t, t, t);
+        assertEquals("java.util.function.BiFunction<T, T, T>", 
TypeUtils.toString(biFunctionType));
+
+        final WildcardType wildcard = 
TypeUtils.wildcardType().withUpperBounds(listString).build();
+        final ParameterizedType mapWildcards = 
TypeUtils.parameterize(Map.class, wildcard, wildcard);
+        assertEquals("java.util.Map<? extends 
java.util.List<java.lang.String>, ? extends java.util.List<java.lang.String>>",
+                TypeUtils.toString(mapWildcards));
+    }
+
+    @Test
+    void testThreadLocalCleanupAfterException() {
+        final Type unsupportedType = new Type() {
+            @Override
+            public String getTypeName() {
+                return "Unsupported";
+            }
+        };
+        assertThrows(IllegalArgumentException.class, () -> 
TypeUtils.toString(unsupportedType));
+        assertEquals("java.lang.String", TypeUtils.toString(String.class));
+
+        final Type faultyType = new ParameterizedType() {
+            @Override
+            public Type[] getActualTypeArguments() {
+                throw new IllegalStateException("Simulated failure in 
getActualTypeArguments");
+            }
+
+            @Override
+            public Type getRawType() {
+                return List.class;
+            }
+
+            @Override
+            public Type getOwnerType() {
+                return null;
+            }
+        };
+        assertThrows(IllegalStateException.class, () -> 
TypeUtils.toString(faultyType));
+        assertEquals("java.lang.String", TypeUtils.toString(String.class));
+
+        final ParameterizedType wrapper = new ParameterizedType() {
+            @Override
+            public Type[] getActualTypeArguments() {
+                return new Type[] { faultyType };
+            }
+
+            @Override
+            public Type getRawType() {
+                return Set.class;
+            }
+
+            @Override
+            public Type getOwnerType() {
+                return null;
+            }
+        };
+        assertThrows(IllegalStateException.class, () -> 
TypeUtils.toString(wrapper));
+        assertEquals("java.util.List<java.lang.String>",
+                TypeUtils.toString(TypeUtils.parameterize(List.class, 
String.class)));
+    }
+
+    @Test
+    void testCyclicWildcardTypeToString() {
+        final WildcardType[] holder = new WildcardType[1];
+        final WildcardType cyclicWildcard = new WildcardType() {
+            @Override
+            public Type[] getUpperBounds() {
+                return new Type[] { holder[0] };
+            }
+
+            @Override
+            public Type[] getLowerBounds() {
+                return ArrayUtils.EMPTY_TYPE_ARRAY;
+            }
+        };
+        holder[0] = cyclicWildcard;
+        assertEquals("? extends ? (cycle)", 
TypeUtils.toString(cyclicWildcard));
+    }
+
+    @Test
+    void testCyclicParameterizedTypeToString() {
+        final ParameterizedType[] holder = new ParameterizedType[1];
+        final ParameterizedType cyclicType = new ParameterizedType() {
+            @Override
+            public Type[] getActualTypeArguments() {
+                return new Type[] { holder[0] };
+            }
+
+            @Override
+            public Type getRawType() {
+                return List.class;
+            }
+
+            @Override
+            public Type getOwnerType() {
+                return null;
+            }
+        };
+        holder[0] = cyclicType;
+        assertEquals("java.util.List<List(cycle)>", 
TypeUtils.toString(cyclicType));
+    }
+
+    @Test
+    void testCyclicGenericArrayTypeToString() {
+        final GenericArrayType[] holder = new GenericArrayType[1];
+        final GenericArrayType cyclicType = () -> holder[0];
+        holder[0] = cyclicType;
+        assertEquals("(cycle)[]", TypeUtils.toString(cyclicType));
+    }
+
 }

Reply via email to