INRIA / INRIA/spoon

[Bug]: Bounded wildcard erasure recursively overflows

Open
#6,802 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
2k
Forks
392
Avg merge
11h 24m
Merged PRs (30d)
36

Description

## 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:

1. Spoon tries to find a declaration named `?` for the wildcard.
2. Resolving the bound `E` asks for the containing method declaration.
3. 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:

```java
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 extends AbstractSet {
private final Set values;

TypeParameterErasureRecursion(Set values) {
this.values = values;
}

@Override
public Iterator iterator() {
return new Iterator<>() {
private final Iterator 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[] toArray(A[] array) {
Object[] result = array;
for (U value : this) {
result[0] = value;
}
return array;
}
}

abstract class GenericMap {
private final Map delegate = null;

Set> entrySet() {
return new TypeParameterErasureRecursion<>(delegate.entrySet()) {
@Override
protected Map.Entry unwrap(Map.Entry value) {
return value;
}
};
}
}

class GenericLambda {
static void mapAll(
Collection values,
Function mapper) {
values.forEach(value -> consume(mapper.apply(value), value));
}

static void consume(MissingKey key, T value) {
}
}
```

The investigation started from a historical StoneDetector stack overflow while traversing [Elasticsearch](https://github.com/elastic/elasticsearch) [AttributeMap.java](https://github.com/elastic/elasticsearch/blob/1b1acdd6b81464d626ce17c6899ecf6d38f819e3/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/expression/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

```java
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

```text
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](https://github.com/openjdk/jdk) 25.0.3

## What operating system are you using?

Linux x86-64

Contributor guide

Open the contributing guide

Research direction

Start with the reduced no-classpath source and the Launcher model-traversal entry point, then inspect the stack frames involving CtTypeParameterReferenceImpl, CtTypeImpl, and CtExecutableReferenceImpl. Reproduce the StackOverflowError on Spoon master and trace wildcard erasure and executable lookup through nested and sibling type parameters. Done means traversal terminates, upper and lower wildcard erasure follow the stated rules, and the reference ownership and lookup paths remain correct.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
compilers, devtools
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.