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));
+ }
+
}