From bdf9e4803c97618a816e4c36975175c9ba22ac29 Mon Sep 17 00:00:00 2001 From: alwaysgaurav1 Date: Sat, 12 Sep 2026 00:25:31 +0530 Subject: [PATCH 1/3] 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 - 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 --- .../commons/lang3/reflect/TypeUtils.java | 100 ++++++++++-------- .../commons/lang3/reflect/TypeUtilsTest.java | 67 ++++++++++++ 2 files changed, 120 insertions(+), 47 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 92dfff7dae1..a478c42d1b3 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java @@ -290,7 +290,7 @@ public String toString() { // @formatter:off private static final AppendableJoiner AMP_JOINER = AppendableJoiner.builder() .setDelimiter(" & ") - .setElementAppender((a, e) -> a.append(toString(e))) + .setElementAppender((a, e) -> a.append(toReferenceString(e))) .get(); // @formatter:on @@ -316,6 +316,18 @@ public String toString() { .get(); // @formatter:on + /** + * Type arguments joiner. + */ + // @formatter:off + private static final AppendableJoiner TYPE_ARG_JOINER = AppendableJoiner.builder() + .setPrefix("<") + .setSuffix(">") + .setDelimiter(", ") + .setElementAppender((a, e) -> a.append(toReferenceString(e))) + .get(); + // @formatter:on + /** * A wildcard instance matching {@code ?}. * @@ -327,17 +339,20 @@ private static 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 +402,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 +420,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 +572,6 @@ private static Type[] extractTypeArgumentsFrom(final Map, 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 +591,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())); } /** @@ -1450,11 +1460,9 @@ private static String parameterizedTypeToString(final ParameterizedType paramete } 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(); } @@ -1605,6 +1613,8 @@ public static boolean typesSatisfyVariables(final Map, Type> typ return true; } + private static final ThreadLocal>> VISITING = ThreadLocal.withInitial(HashSet::new); + /** * Formats a {@link TypeVariable} as a {@link String}. * @@ -1613,23 +1623,19 @@ public static boolean typesSatisfyVariables(final Map, Type> typ */ 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; + final Set> visiting = VISITING.get(); + if (visiting.add(typeVariable)) { + try { + final Type[] bounds = typeVariable.getBounds(); + if (bounds.length > 0 && !(bounds.length == 1 && Object.class.equals(bounds[0]))) { + builder.append(" extends "); + AMP_JOINER.join(builder, bounds); + } + } finally { + visiting.remove(typeVariable); + if (visiting.isEmpty()) { + VISITING.remove(); } - } - if (append) { - 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 5dc6d15f9c8..5713d913c72 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java @@ -171,6 +171,26 @@ abstract class Test1 { public abstract Map m9(); } +class MySuperClass { + // empty +} + +class MyClass> { + // empty +} + +class MultiBoundClass> { + // empty +} + +class TwoParams, U> { + // empty +} + +class InterfaceBound> { + // empty +} + /** * Tests {@link TypeUtils}. * @@ -1296,4 +1316,51 @@ void testWrap() { assertEquals(String.class, TypeUtils.wrap(String.class).getType()); } + @Test + void testRecursiveTypeWildcardBoundClass() { + assertEquals("org.apache.commons.lang3.reflect.MyClass>", + TypeUtils.toString(MyClass.class)); + assertEquals("U extends org.apache.commons.lang3.reflect.MySuperClass", + TypeUtils.toString(MyClass.class.getTypeParameters()[0])); + } + + @Test + void testMultiBoundRecursiveType() { + assertEquals("org.apache.commons.lang3.reflect.MultiBoundClass>", + TypeUtils.toString(MultiBoundClass.class)); + assertEquals("U extends java.lang.Number & java.lang.Comparable", + TypeUtils.toString(MultiBoundClass.class.getTypeParameters()[0])); + } + + @Test + void testInterfaceBoundPreserved() { + assertEquals("T extends java.util.List", + 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", + TypeUtils.toString(parameterizedType)); + } + + @Test + void testGClassToString() { + assertEquals("org.apache.commons.lang3.reflect.AClass.GClass " + + "& org.apache.commons.lang3.reflect.AClass.AInterface>>", + 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)); + } + } From e07636f31cb2ee833f027087acef25191c612b20 Mon Sep 17 00:00:00 2001 From: alwaysgaurav1 Date: Sat, 12 Sep 2026 22:49:37 +0530 Subject: [PATCH 2/3] Cover all Type nodes with identity-based recursion guard and defensively copy bounds in WildcardTypeImpl --- .../commons/lang3/reflect/TypeUtils.java | 88 ++++++++++++------- .../commons/lang3/reflect/TypeUtilsTest.java | 58 ++++++++++++ 2 files changed, 114 insertions(+), 32 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 a478c42d1b3..3eebcdf538b 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; } /** @@ -1560,6 +1561,30 @@ public static String toLongString(final TypeVariable typeVariable) { return buf.append(':').append(typeVariableToString(typeVariable)).toString(); } + private static final ThreadLocal> 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)"; + } + /** * Formats a given type as a Java-esque String. * @@ -1570,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); - } - if (type instanceof TypeVariable) { - return typeVariableToString((TypeVariable) type); + final Set visiting = VISITING.get(); + if (!visiting.add(type)) { + return toCyclicString(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)); } /** @@ -1613,8 +1649,6 @@ public static boolean typesSatisfyVariables(final Map, Type> typ return true; } - private static final ThreadLocal>> VISITING = ThreadLocal.withInitial(HashSet::new); - /** * Formats a {@link TypeVariable} as a {@link String}. * @@ -1623,20 +1657,10 @@ public static boolean typesSatisfyVariables(final Map, Type> typ */ private static String typeVariableToString(final TypeVariable typeVariable) { final StringBuilder builder = new StringBuilder(typeVariable.getName()); - final Set> visiting = VISITING.get(); - if (visiting.add(typeVariable)) { - try { - final Type[] bounds = typeVariable.getBounds(); - if (bounds.length > 0 && !(bounds.length == 1 && Object.class.equals(bounds[0]))) { - builder.append(" extends "); - AMP_JOINER.join(builder, bounds); - } - } finally { - visiting.remove(typeVariable); - if (visiting.isEmpty()) { - VISITING.remove(); - } - } + final Type[] bounds = typeVariable.getBounds(); + if (bounds.length > 0 && !(bounds.length == 1 && Object.class.equals(bounds[0]))) { + 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 5713d913c72..5edc3137128 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java @@ -51,6 +51,7 @@ 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; @@ -1363,4 +1364,61 @@ void testContainsTypeVariablesMultiBoundWildcard() { assertFalse(TypeUtils.containsTypeVariables(wtNone)); } + @Test + void testWildcardTypeBuilderDefensiveCopy() { + final Type[] bounds = { String.class }; + final WildcardType wildcard = TypeUtils.wildcardType().withUpperBounds(bounds).build(); + bounds[0] = Integer.class; + assertArrayEquals(new Type[] { String.class }, wildcard.getUpperBounds()); + } + + @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", 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)); + } + } From df5c62c1aa10cd141603f09839062f21966cbe65 Mon Sep 17 00:00:00 2001 From: alwaysgaurav1 Date: Sun, 13 Sep 2026 13:24:08 +0530 Subject: [PATCH 3/3] Add tests and safeguards for review feedback on TypeUtils formatting - Add tests for bounded T[], List, and 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 --- .../commons/lang3/reflect/TypeUtils.java | 4 +- .../commons/lang3/reflect/TypeUtilsTest.java | 215 +++++++++++++++++- 2 files changed, 213 insertions(+), 6 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 3eebcdf538b..4c8491efd4a 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java @@ -1457,7 +1457,7 @@ 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()); } @@ -1558,7 +1558,7 @@ 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> VISITING = ThreadLocal.withInitial(() -> Collections.newSetFromMap(new IdentityHashMap<>())); 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 5edc3137128..6c00f820410 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,7 +48,9 @@ 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; @@ -192,6 +195,20 @@ class InterfaceBound> { // empty } +class DependentBounds { + // empty +} + +class ParameterizedOwner { + class NonGenericInner { + // empty + } + + public NonGenericInner getInner() { + return null; + } +} + /** * Tests {@link TypeUtils}. * @@ -1366,10 +1383,200 @@ void testContainsTypeVariablesMultiBoundWildcard() { @Test void testWildcardTypeBuilderDefensiveCopy() { - final Type[] bounds = { String.class }; - final WildcardType wildcard = TypeUtils.wildcardType().withUpperBounds(bounds).build(); - bounds[0] = Integer.class; - assertArrayEquals(new Type[] { String.class }, wildcard.getUpperBounds()); + // 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", TypeUtils.toString(pt)); + } + + @Test + void testDependentBoundsClassAndTypeParametersToString() { + assertEquals("org.apache.commons.lang3.reflect.DependentBounds", + 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", + TypeUtils.toLongString(MultiBoundClass.class.getTypeParameters()[0])); + assertEquals("org.apache.commons.lang3.reflect.InterfaceBound:T extends java.util.List", + TypeUtils.toLongString(InterfaceBound.class.getTypeParameters()[0])); + assertEquals("org.apache.commons.lang3.reflect.MyClass:U extends org.apache.commons.lang3.reflect.MySuperClass", + 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.NonGenericInner", + TypeUtils.toString(nonGenericInner)); + + final Type methodReturnType = ParameterizedOwner.class.getMethod("getInner").getGenericReturnType(); + assertEquals("org.apache.commons.lang3.reflect.ParameterizedOwner.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>", + 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", 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>", + 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", + TypeUtils.toString(TypeUtils.parameterize(List.class, String.class))); } @Test