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 {