Skip to content

Commit

Permalink
Merge pull request #53 from lorenzleutgeb/reify-fix
Browse files Browse the repository at this point in the history
Non-equality on recursive resolved types overflows stack
  • Loading branch information
jhalterman committed Feb 26, 2020
1 parent b469bc4 commit f992cd6
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 69 deletions.
90 changes: 73 additions & 17 deletions src/main/java/net/jodah/typetools/ReifiedParameterizedType.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,35 @@

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;

class ReifiedParameterizedType implements ParameterizedType {
private final ParameterizedType original;
private final Type[] reifiedTypeArguments;
private final boolean[] loop;
private int reified = 0;

ReifiedParameterizedType(ParameterizedType original) {
this.original = original;
this.reifiedTypeArguments = new Type[original.getActualTypeArguments().length];
this.loop = new boolean[original.getActualTypeArguments().length];
}

/**
* This method is used to set reified types as they are processed. For example,
* When reifying some {@code T<E1, E2>}, in order to reify {@code T} we need
* to reify first {@code E1} and then {@code E2} in order. The reified counterpart
* of {@code T} is allocated before, and then the results from reifying {@code E1}
* and {@code E2} are added through this method.
* @param type the reification result to be added
*/
/* package-private */ void addReifiedTypeArgument(Type type) {
if (reified >= reifiedTypeArguments.length) {
return;
}
if (type == this) {
loop[reified] = true;
}
reifiedTypeArguments[reified++] = type;
}

@Override
Expand All @@ -29,20 +49,15 @@ public Type getOwnerType() {
}

/**
* NOTE: This method should only be called once per instance.
* Keep this consistent with {@link sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl#toString}
*/
void setReifiedTypeArguments(Type[] reifiedTypeArguments) {
System.arraycopy(reifiedTypeArguments, 0, this.reifiedTypeArguments, 0, this.reifiedTypeArguments.length);
}

/** Keep this consistent with {@link sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl#toString} */
@Override
public String toString() {
final Type ownerType = getOwnerType();
final Type rawType = getRawType();
final Type[] actualTypeArguments = getActualTypeArguments();

StringBuilder sb = new StringBuilder();
final StringBuilder sb = new StringBuilder();

if (ownerType != null) {
if (ownerType instanceof Class) {
Expand All @@ -51,13 +66,15 @@ public String toString() {
sb.append(ownerType.toString());
}

sb.append(".");
sb.append("$");

if (ownerType instanceof ParameterizedType) {
// Find simple name of nested type by removing the
// shared prefix with owner.
sb.append(rawType.getTypeName()
.replace(((ParameterizedType) ownerType).getRawType().getTypeName() + "$", ""));
} else if (rawType instanceof Class){
sb.append(((Class) rawType).getSimpleName());
} else {
sb.append(rawType.getTypeName());
}
Expand All @@ -68,13 +85,27 @@ public String toString() {
if (actualTypeArguments != null && actualTypeArguments.length > 0) {
sb.append("<");

boolean first = true;
for (Type t: actualTypeArguments) {
if (!first) {
for (int i = 0; i < actualTypeArguments.length; i++) {
if (i != 0) {
sb.append(", ");
}
sb.append(t == null ? "null" : t.getTypeName());
first = false;

final Type t = actualTypeArguments[i];

if (i >= reified) {
sb.append("?");
} else if (t == null) {
sb.append("null");
} else if (loop[i]) {
// Instead of recursing into this argument, which would overflow the stack,
// print three dots to indicate "self-loop structure is here". Note
// that if the full string examined is some other type that contains this
// instance, then the notation is ambiguous: We don't know which type
// is self-loop where.
sb.append("...");
} else {
sb.append(t.getTypeName());
}
}
sb.append(">");
}
Expand All @@ -92,14 +123,39 @@ public boolean equals(Object o) {
}

ReifiedParameterizedType that = (ReifiedParameterizedType) o;
return original.equals(that.original) &&
Arrays.equals(reifiedTypeArguments, that.reifiedTypeArguments);
if (!original.equals(that.original)) {
return false;
}

if (reifiedTypeArguments.length != that.reifiedTypeArguments.length) {
return false;
}

for (int i = 0; i < reifiedTypeArguments.length; i++) {
if (loop[i] != that.loop[i]) {
return false;
}
if (loop[i]) {
continue;
}
if (reifiedTypeArguments[i] != that.reifiedTypeArguments[i]) {
return false;
}
}
return true;
}

@Override
public int hashCode() {
int result = original.hashCode();
result = 31 * result + Arrays.hashCode(reifiedTypeArguments);
for (int i = 0; i < reifiedTypeArguments.length; i++) {
if (loop[i]) {
continue;
}
if (reifiedTypeArguments[i] instanceof ReifiedParameterizedType) {
result = 31 * result + reifiedTypeArguments[i].hashCode();
}
}
return result;
}
}
73 changes: 27 additions & 46 deletions src/main/java/net/jodah/typetools/TypeResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -399,55 +399,46 @@ private static Type reify(final Type genericType, final Map<TypeVariable<?>, Typ
else if (genericType instanceof Class<?>)
return genericType;
else
return reify(genericType, typeVariableTypeMap, new HashMap<Type, Type>());
return reify(genericType, typeVariableTypeMap, new HashMap<ParameterizedType, ReifiedParameterizedType>());
}

/**
* Works like {@link #resolveRawClass(Type, Class, Class)} but does not stop at raw classes. Instead, traverses
* referenced types.
*
* @param cache contains a mapping of generic types to reified types. A value of {@code null} inside a
* @param partial contains a mapping of generic types to reified types. A value of {@code null} inside a
* {@link ReifiedParameterizedType} instance means that this type is currently being reified.
*/
private static Type reify(Type genericType, final Map<TypeVariable<?>, Type> typeVariableMap, Map<Type, Type> cache) {
private static Type reify(Type genericType, final Map<TypeVariable<?>, Type> typeVariableMap, Map<ParameterizedType, ReifiedParameterizedType> partial) {
// Terminal case.
if (genericType instanceof Class<?>)
return genericType;

// For cycles of length larger than one, find its last element by chasing through cache.
while (cache.containsKey(genericType)) {
genericType = cache.get(genericType);
}

// Recursive cases.
if (genericType instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) genericType;
// Self-referential type needs special attention. Otherwise we might accidentally overflow the stack.
if (partial.containsKey(parameterizedType)) {
ReifiedParameterizedType res = partial.get(genericType);
res.addReifiedTypeArgument(res);
return res;
}
final Type[] genericTypeArguments = parameterizedType.getActualTypeArguments();
final Type[] reifiedTypeArguments = new Type[genericTypeArguments.length];

ReifiedParameterizedType result = new ReifiedParameterizedType(parameterizedType);
cache.put(genericType, result);

boolean changed = false;
for (int i = 0; i < genericTypeArguments.length; i++) {
// Cycle detection. In case a genericTypeArgument is null, it is currently being resolved,
// thus there's a cycle in the type's structure.
if (genericTypeArguments[i] == null) {
return parameterizedType;
final ReifiedParameterizedType result = new ReifiedParameterizedType(parameterizedType);
partial.put(parameterizedType, result);
for (Type genericTypeArgument : genericTypeArguments) {
Type reified = reify(genericTypeArgument, typeVariableMap, partial);
// Self-references are added as soon as they are detected, see above.
// In this case, skip adding.
if (reified != result) {
result.addReifiedTypeArgument(reified);
}
reifiedTypeArguments[i] = reify(genericTypeArguments[i], typeVariableMap, cache);
changed = changed || (reifiedTypeArguments[i] != genericTypeArguments[i]);
}

if (!changed)
return parameterizedType;

result.setReifiedTypeArguments(reifiedTypeArguments);
return result;
} else if (genericType instanceof GenericArrayType) {
final GenericArrayType genericArrayType = (GenericArrayType) genericType;
final Type genericComponentType = genericArrayType.getGenericComponentType();
final Type reifiedComponentType = reify(genericArrayType.getGenericComponentType(), typeVariableMap, cache);
final Type reifiedComponentType = reify(genericArrayType.getGenericComponentType(), typeVariableMap, partial);

if (genericComponentType == reifiedComponentType)
return genericComponentType;
Expand All @@ -461,36 +452,26 @@ private static Type reify(Type genericType, final Map<TypeVariable<?>, Type> typ
} else if (genericType instanceof TypeVariable<?>) {
final TypeVariable<?> typeVariable = (TypeVariable<?>) genericType;
final Type mapping = typeVariableMap.get(typeVariable);
if (mapping != null) {
cache.put(typeVariable, mapping);
return reify(mapping, typeVariableMap, cache);
}

final Type[] upperBounds = typeVariable.getBounds();

// Copy cache in case the bound is mutually recursive on the variable. This is to avoid sharing of
// cache in different branches of the call-graph of reify.
cache = new HashMap<Type, Type>(cache);

if (mapping != null)
return reify(mapping, typeVariableMap, partial);
// NOTE: According to https://docs.oracle.com/javase/tutorial/java/generics/bounded.html
// if there are multiple upper bounds where one bound is a class, then this must be the
// leftmost/first bound. Therefore we blindly take this one, hoping is the most relevant.
// leftmost/first bound. Therefore we blindly take this one, hoping it is the most relevant.
// Hibernate does the same when erasing types, see also
// https://github.com/hibernate/hibernate-validator/blob/6.0/engine/src/main/java/org/hibernate/validator/internal/util/TypeHelper.java#L181-L186
cache.put(typeVariable, upperBounds[0]);
return reify(upperBounds[0], typeVariableMap, cache);
return reify(typeVariable.getBounds()[0], typeVariableMap, partial);
} else if (genericType instanceof WildcardType) {
final WildcardType wildcardType = (WildcardType) genericType;
final Type[] upperBounds = wildcardType.getUpperBounds();
final Type[] lowerBounds = wildcardType.getLowerBounds();
if (upperBounds.length == 1 && lowerBounds.length == 0)
return reify(upperBounds[0], typeVariableMap, cache);
return reify(upperBounds[0], typeVariableMap, partial);

throw new UnsupportedOperationException(
"Attempted to reify wildcard type with name '" + wildcardType + "' which has " +
upperBounds.length + " upper bounds and " + lowerBounds.length + " lower bounds. " +
"Reification of wildcard types is only supported for " +
"the trivial case of exactly one upper bound and no lower bounds.");
"Attempted to reify wildcard type with name '" + wildcardType.getTypeName() +
"' which has " + upperBounds.length + " upper bounds and " + lowerBounds.length +
" lower bounds. Reification of wildcard types is only supported for" +
" the trivial case of exactly 1 upper bound and 0 lower bounds.");
}
throw new UnsupportedOperationException(
"Reification of type with name '" + genericType.getTypeName() + "' and " +
Expand Down
56 changes: 50 additions & 6 deletions src/test/java/net/jodah/typetools/TypeResolverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
Expand Down Expand Up @@ -119,6 +120,13 @@ public void shouldResolveParameterizedTypeForBazFromFoo() {
assert secondTypeArgument.getActualTypeArguments()[0] == Object.class;
}

public void shouldResolveTypeVariable() {
TypeVariable<Class<Baz>> typeVariable = Baz.class.getTypeParameters()[0];
Type type = TypeResolver.reify(typeVariable, Bar.class);
assert type instanceof ParameterizedType;
assert ((ParameterizedType) type).getRawType().equals(HashSet.class);
}

public void shouldResolvePartialParameterizedTypeForBazFromBar() {
Type type = TypeResolver.reify(Baz.class, Bar.class);

Expand Down Expand Up @@ -319,6 +327,7 @@ public void shouldReifyWildcardWithComplexUpperBound() throws Exception {
}

static abstract class EnumBound<S extends Enum<S>> {
public S enumField;
}

static abstract class SubEnumBound<S extends Enum<S>> extends EnumBound<S> {
Expand All @@ -334,7 +343,7 @@ static abstract class RecursiveOnSecond<S, T extends RecursiveOnSecond<S, T>> ex

static abstract class RecursiveLongBase<T> {}

static abstract class RecursiveLong<T extends List<List<T>>> {}
static abstract class RecursiveLong<T extends List<Set<T>>> extends RecursiveLongBase<T> {}

public void shouldReifyRecursiveBound() {
Type result = TypeResolver.reify(EnumBound.class, SubEnumBound.class);
Expand All @@ -343,23 +352,58 @@ public void shouldReifyRecursiveBound() {

// Navigate into enum parameter.
assert parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType;
parameterizedType = (ParameterizedType)parameterizedType.getActualTypeArguments()[0];
ParameterizedType parameterizedType2 = (ParameterizedType)parameterizedType.getActualTypeArguments()[0];
assert parameterizedType.getActualTypeArguments()[0].equals(parameterizedType2);
// Assert existence of loop
assert parameterizedType.getActualTypeArguments()[0] == parameterizedType;
assert parameterizedType2.getActualTypeArguments()[0] == parameterizedType2;

assert !parameterizedType.equals(parameterizedType2);
}

public void shouldReifyEnumBound() throws NoSuchFieldException {
Type result = TypeResolver.reify(SubEnumBound.class.getField("enumField").getGenericType(), SubEnumBound.class);
assert result instanceof ParameterizedType;
ParameterizedType parameterizedType = (ParameterizedType) result;
assert parameterizedType.toString().equals("java.lang.Enum<...>");

// Navigate into enum parameter.
assert parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType;
ParameterizedType parameterizedType2 = (ParameterizedType)parameterizedType.getActualTypeArguments()[0];
assert parameterizedType.getActualTypeArguments()[0].equals(parameterizedType2);

// Assert existence of loop
assert parameterizedType2.getActualTypeArguments()[0] == parameterizedType2;

// Reify the same type again, which will create new instances of
// ReifiedParameterizedType that are not referentially equal, but
// equal to the previous result.
Type same = TypeResolver.reify(SubEnumBound.class.getField("enumField").getGenericType(), SubEnumBound.class);
assert same.equals(result);

// Just to call hashCode() and equals() a bit more.
Set<Type> types = new HashSet<>();
types.add(parameterizedType);
types.add(parameterizedType2);
types.add(same);
assert types.size() == 1;
assert types.contains(same);
}

public void shouldReifyMutuallyRecursiveBound() {
Type result = TypeResolver.reify(MutuallyRecursiveBase.class, MutuallyRecursive.class);
assert result instanceof ParameterizedType;
ParameterizedType parent = (ParameterizedType) result;

assert result.toString().equals("net.jodah.typetools.TypeResolverTest$MutuallyRecursiveBase<java.util.List<...>, java.util.List<...>>");
assert parent.getActualTypeArguments()[0] instanceof ParameterizedType;
ParameterizedType parameterizedType = (ParameterizedType)parent.getActualTypeArguments()[0];
assert parameterizedType.getActualTypeArguments()[0] == parameterizedType;
assert parameterizedType.getActualTypeArguments()[0].equals(parameterizedType);

assert parent.getActualTypeArguments()[1] instanceof ParameterizedType;
parameterizedType = (ParameterizedType)parent.getActualTypeArguments()[1];
assert parameterizedType.getActualTypeArguments()[0] == parameterizedType;
ParameterizedType parameterizedType2 = (ParameterizedType)parent.getActualTypeArguments()[1];
assert parameterizedType2.getActualTypeArguments()[0] == parameterizedType2;

assert parameterizedType.equals(parameterizedType2);
}

public void shouldReifyRecursiveOnSecondBound() {
Expand Down

0 comments on commit f992cd6

Please sign in to comment.