Skip to content

[Bug]: Bounded wildcard erasure recursively overflows #6802

Description

@martinfrancois

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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions