Skip to content

Inconsistent handling of self-references (ResolvedRecursiveType) in caching and equality #128

Closed

Description

@cowtowncoder

Follow-up from review of the #125 fix: handling of self-references (ResolvedRecursiveType) is inconsistent in two ways.

1. Cached types may contain self-references from another resolution context

A type without type parameters is cached under a single key, but when it is first resolved as a supertype of a type that refers back to it, its members contain self-references to that type (with its parameterization). A later direct resolution returns the cached instance, so results depend on resolution order:

static abstract class Bar implements Comparable<Foo> { }  // raw Foo
static abstract class Foo<T> extends Bar { }

TypeResolver r = new TypeResolver();
// with fresh resolver: Comparable<Foo<Object>> (ResolvedObjectType)
r.resolve(Foo.class, String.class);
r.resolve(Bar.class).typeParametersFor(Comparable.class).get(0);
// -> ResolvedRecursiveType that refers to Foo<String>

Before #125 this was hidden for generic subtypes (supertypes were cached per enclosing parameterization), but the same already happens on 1.7 for non-generic subtypes. As of 1.8, types that have self-references in their type parameters are no longer cached, but self-references in supertypes are not checked.

2. Self-reference vs resolved type equality is only relaxed for array elements

As of 1.8, ResolvedArrayType.equals() treats a self-referential element type as equal to the fully resolved type (by erased type and bindings), so that f.ex Node[] within class Node extends Base<Node[]> equals resolve(Node[].class) (needed to detect method overrides). But the same is not done for type parameters: Base<Node> reached via Node's parent is still not equal to a directly resolved Base<Node>, nor is List<Node>[]. Also, two equal arrays may have element types that are not equal to each other.

Possible approach

Make ResolvedRecursiveType equality (and hash code) consistent with the type it refers to, in general rather than only for array elements; this needs care to avoid infinite recursion (see #117). That would also allow simplifying the array special case.

Activity

  1. cowtowncoder commented on Oct 8, 2026

    @cowtowncoder
    MemberAuthor

    More cases and a suggested design, from further review of the #125 fix.

    More cases

    Self-references via supertypes are still cached (part 1 above, also without raw types):

    static class A extends Base<B> { }
    static class B extends Base<A> { }
    
    r.resolve(A.class);   // caches B, with parent Base<A> where A is a ResolvedRecursiveType
    r.resolve(B.class).getParentClass().getTypeParameters().get(0);
    // -> ResolvedRecursiveType (getParentClass() == null); fresh resolver gives full ResolvedObjectType A

    Same for Mid extends Base<Outer> / Outer extends Mid, and for arrays of such types. (1.8 only avoids caching types with self-references in their type parameters.)

    Equality only relaxed for array elements (part 2 above), still not equal:

    • class LNode extends Base<List<LNode>>: resolve(LNode).getParentClass() vs resolve(Base.class, List<LNode>)
    • class N<T> extends Base<N<N<T>>[]>: parent's N<N<String>>[] vs arrayType(N<N<String>>) (self-reference nested within bindings of the self-reference)
    • class GNode<T> extends Base<GNode[]>: raw self-reference has no bindings, so it is not equal to GNode<Object>[] (relaxing it to "matches any parameterization" was tried and rejected, since it breaks transitivity and with it HashSet/HashMap)

    Suggested design

    1. Caching: instead of scanning TypeBindings for self-references (current 1.8 approach, TypeBindings.hasContextualTypes()), mark ClassStack frames as incomplete when context.find() creates a self-reference to them, and skip caching for types constructed while any such frame is open. That covers bindings, supertypes and arrays with one mechanism; placeholder handling could stay as is.
    2. Equality: handle self-reference vs. resolved type in TypeBindings.equals() (a ResolvedRecursiveType matches a non-recursive type with the same erased type and equal bindings), instead of the special case in ResolvedArrayType.equals(); raw self-references could be given bindings resolved to bounds at creation. Needs care to avoid infinite recursion (see Regression in 1.7.2 (wrt #53) causes StackOverflowError for some recursive types #117).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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