Describe the bug
Type erasure is the simpler type Spoon uses when it compares generic method or constructor signatures. For example, erasing ? extends E requires Spoon to derive a comparison type from its upper bound E.
In the no-classpath reproduction below, that operation enters this cycle:
- Spoon tries to find a declaration named
? for the wildcard.
- Resolving the bound
E asks for the containing method declaration.
- Matching that method against its reference erases the wildcard parameter again.
The same lookup repeats until the JVM throws StackOverflowError.
A compatible fix must define wildcard erasure directly: an upper-bounded wildcard must use its bound's erasure, while a lower-bounded wildcard must use Object. Executable lookup must not re-enter the same executable reference through any nested or sibling type parameter.
After exact method or constructor lookup succeeds, Spoon must follow the same array, wildcard, declaring-type, and generic-argument path from the reference to the corresponding declaration. A reference created from a lexical type parameter must keep that ownership when it is reparented, cloned, or serialized.
Source code you are trying to analyze/transform
This is the complete reduced no-classpath reproduction:
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
abstract class TypeParameterErasureRecursion<W, U> extends AbstractSet<U> {
private final Set<W> values;
TypeParameterErasureRecursion(Set<W> values) {
this.values = values;
}
@Override
public Iterator<U> iterator() {
return new Iterator<>() {
private final Iterator<W> iterator = values.iterator();
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public U next() {
return unwrap(iterator.next());
}
};
}
protected abstract U unwrap(W value);
@Override
@SuppressWarnings("unchecked")
public <A> A[] toArray(A[] array) {
Object[] result = array;
for (U value : this) {
result[0] = value;
}
return array;
}
}
abstract class GenericMap<E> {
private final Map<String, E> delegate = null;
Set<Map.Entry<String, E>> entrySet() {
return new TypeParameterErasureRecursion<>(delegate.entrySet()) {
@Override
protected Map.Entry<String, E> unwrap(Map.Entry<String, E> value) {
return value;
}
};
}
}
class GenericLambda {
static <T> void mapAll(
Collection<? extends T> values,
Function<T, MissingKey> mapper) {
values.forEach(value -> consume(mapper.apply(value), value));
}
static <T> void consume(MissingKey key, T value) {
}
}
The investigation started from a historical StoneDetector stack overflow while traversing Elasticsearch AttributeMap.java. On the audited current Spoon master snapshot, that full downstream file also passes without this change, so it is a compatibility replay rather than a current reproduction. The complete reduced source above is the authoritative reproduction for this issue.
Source code for your Spoon processing
Launcher launcher = new Launcher();
launcher.getEnvironment().setNoClasspath(true);
launcher.addInputResource("TypeParameterErasureRecursion.java");
CtModel model = launcher.buildModel();
model.getElements(element -> true).forEach(Object::toString);
Actual output
java.lang.StackOverflowError
at CtTypeParameterReferenceImpl.getDeclaration(...)
at CtTypeParameterReferenceImpl.getTypeErasure(...)
at CtTypeImpl.isSameParameter(...)
at CtTypeImpl.getMethod(...)
at CtExecutableReferenceImpl.getExecutableDeclaration(...)
... repeats ...
Expected output
Model traversal and wildcard erasure terminate without recursively re-entering the same executable lookup.
Spoon Version
Reproduced on Spoon master at 06b187819380700384762416d15db2cc4f112175 with the reduced regression.
JVM Version
OpenJDK 25.0.3
What operating system are you using?
Linux x86-64
Describe the bug
Type erasure is the simpler type Spoon uses when it compares generic method or constructor signatures. For example, erasing
? extends Erequires Spoon to derive a comparison type from its upper boundE.In the no-classpath reproduction below, that operation enters this cycle:
?for the wildcard.Easks for the containing method declaration.The same lookup repeats until the JVM throws
StackOverflowError.A compatible fix must define wildcard erasure directly: an upper-bounded wildcard must use its bound's erasure, while a lower-bounded wildcard must use
Object. Executable lookup must not re-enter the same executable reference through any nested or sibling type parameter.After exact method or constructor lookup succeeds, Spoon must follow the same array, wildcard, declaring-type, and generic-argument path from the reference to the corresponding declaration. A reference created from a lexical type parameter must keep that ownership when it is reparented, cloned, or serialized.
Source code you are trying to analyze/transform
This is the complete reduced no-classpath reproduction:
The investigation started from a historical StoneDetector stack overflow while traversing Elasticsearch AttributeMap.java. On the audited current Spoon
mastersnapshot, that full downstream file also passes without this change, so it is a compatibility replay rather than a current reproduction. The complete reduced source above is the authoritative reproduction for this issue.Source code for your Spoon processing
Actual output
Expected output
Model traversal and wildcard erasure terminate without recursively re-entering the same executable lookup.
Spoon Version
Reproduced on Spoon
masterat06b187819380700384762416d15db2cc4f112175with the reduced regression.JVM Version
OpenJDK 25.0.3
What operating system are you using?
Linux x86-64