From 6422051eba2ef41bcd59ea17677db7133b716ce1 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 11:35:56 -0700 Subject: [PATCH 01/19] Fix #128: Self-referencial type checking problems --- VERSION.txt | 16 +- .../com/fasterxml/classmate/ResolvedType.java | 20 +++ .../com/fasterxml/classmate/TypeBindings.java | 3 +- .../com/fasterxml/classmate/TypeResolver.java | 144 ++++++++++----- .../classmate/types/ResolvedArrayType.java | 17 +- .../fasterxml/classmate/util/ClassStack.java | 146 +++++++++++++++ .../classmate/TypeResolver128Test.java | 170 ++++++++++++++++++ .../types/ResolvedArrayTypeTest.java | 13 +- 8 files changed, 468 insertions(+), 61 deletions(-) create mode 100644 src/test/java/com/fasterxml/classmate/TypeResolver128Test.java diff --git a/VERSION.txt b/VERSION.txt index e67d285..466ef7e 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -19,8 +19,7 @@ Release notes: - Array types are cached by `TypeResolver` using element type as part of the key - Types with `TypePlaceHolder`s (from `resolveSubtype()`) or self-references (`ResolvedRecursiveType`) in their type parameters (at any level of nesting, - including array element types) are no longer cached (but see #128 for - self-references via supertypes) + including array element types) are no longer cached #127: `resolveSubtype()` does not verify or resolve array element types - `resolveSubtype()` now rejects type variable bound to conflicting types (like `E` in `Dup extends Pair`; except that `Object`, which may @@ -36,6 +35,19 @@ Release notes: `IllegalArgumentException` if any of type parameters is a primitive type (like `resolve(List.class, int.class)`) (fix by @cowtowncoder w/ Claude code) +#128: Self-references via supertypes still cached; self-references not equal + to fully resolved types +- Types containing self-references to types still being resolved (like `B` in + `B extends Base`, when resolving `A extends Base`; via type parameters, + supertypes or array element types) are no longer cached +- Self-references (`ResolvedRecursiveType`) within type parameters or array element + types are equal to fully resolved types with same erased type and type bindings + (at any level of nesting; f.ex `Base>` from `LNode extends Base>`) +- Raw self-references (like `GNode` in `GNode extends Base`) have their + type parameters resolved to bounds, same as other raw types +- `resolveSubtype()` resolves self-references with bindings different from those + of the referenced type (like `N>` within `N`) using their own bindings + (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/ResolvedType.java b/src/main/java/com/fasterxml/classmate/ResolvedType.java index 6c2beac..9d0c28f 100644 --- a/src/main/java/com/fasterxml/classmate/ResolvedType.java +++ b/src/main/java/com/fasterxml/classmate/ResolvedType.java @@ -8,6 +8,7 @@ import java.util.*; import com.fasterxml.classmate.members.*; +import com.fasterxml.classmate.types.ResolvedRecursiveType; public abstract class ResolvedType implements Type @@ -280,6 +281,25 @@ public String getBriefDescription() { // and type bindings must match as well return _typeBindings.equals(other._typeBindings); } + + /** + * Helper method for comparing types contained in other types (as type parameters + * or array element types): differs from {@link #equals} in that a self-reference + * ({@link ResolvedRecursiveType}) matches a fully resolved type with the same erased + * type and type bindings. + * + * @since 1.8 + */ + protected static boolean _equalTypes(ResolvedType t1, ResolvedType t2) + { + if (t1 == t2) return true; + if ((t1 == null) || (t2 == null)) return false; + if ((t1 instanceof ResolvedRecursiveType) != (t2 instanceof ResolvedRecursiveType)) { + return (t1._erasedType == t2._erasedType) + && t1._typeBindings.equals(t2._typeBindings); + } + return t1.equals(t2); + } /* /********************************************************************** diff --git a/src/main/java/com/fasterxml/classmate/TypeBindings.java b/src/main/java/com/fasterxml/classmate/TypeBindings.java index 5e36d07..ff95967 100644 --- a/src/main/java/com/fasterxml/classmate/TypeBindings.java +++ b/src/main/java/com/fasterxml/classmate/TypeBindings.java @@ -295,7 +295,8 @@ public boolean hasUnbound(String name) { } ResolvedType[] otherTypes = other._types; for (int i = 0; i < len; ++i) { - if (!otherTypes[i].equals(_types[i])) { + // [classmate#128]: self-references must match fully resolved types + if (!ResolvedType._equalTypes(otherTypes[i], _types[i])) { return false; } } diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index a4bd60c..46b907b 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -3,7 +3,6 @@ import java.io.Serializable; import java.lang.reflect.*; import java.util.*; -import java.util.function.Supplier; import com.fasterxml.classmate.types.*; import com.fasterxml.classmate.util.ClassKey; @@ -176,7 +175,7 @@ public ResolvedType resolve(Type type, Type... typeParameters) public ResolvedArrayType arrayType(Type elementType) { ResolvedType resolvedElementType = resolve(TypeBindings.emptyBindings(), elementType); - return _arrayOf(_arrayClassFor(resolvedElementType), resolvedElementType); + return _arrayOf(null, _arrayClassFor(resolvedElementType), resolvedElementType); } /** @@ -370,7 +369,7 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // [classmate#125]: Arrays have no type parameters of their own, so bindings // must not be retained (nor used for element type) if (rawType.isArray()) { - return _arrayOf(rawType, _fromClass(context, rawType.getComponentType(), + return _arrayOf(context, rawType, _fromClass(context, rawType.getComponentType(), TypeBindings.emptyBindings())); } // Second: recursive reference? @@ -380,8 +379,18 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin ClassStack prev = context.find(rawType); if (prev != null) { // Self-reference: needs special handling, then... + // [classmate#128]: raw self-reference gets type parameters resolved to + // their bounds, same as other raw types (unless already resolving bounds + // of the type, to avoid infinite recursion) + if (typeBindings.isEmpty() && (rawType.getTypeParameters().length > 0) + && !prev.isResolvingBounds()) { + typeBindings = TypeBindings.create(rawType, + _resolveBounds(context, prev, rawType)); + } ResolvedRecursiveType selfRef = new ResolvedRecursiveType(rawType, typeBindings); - prev.addSelfReference(selfRef); + // [classmate#128]: also need to keep track of types containing self-references + // (to types still being resolved) to avoid caching them + context.selfReferenceCreated(selfRef, prev); return selfRef; } // no, can just add @@ -395,24 +404,18 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // within resolution context) ResolvedTypeKey key = typeBindings.hasContextualTypes() ? null : _resolvedTypes.key(rawType, typeBindings.typeParameterArray()); - final ClassStack typeContext = context; - type = _findOrConstruct(key, () -> _constructType(typeContext, rawType, typeBindings)); - context.resolveSelfReferences(type); - return type; - } - - /** - * Helper method for finding cached type with given key, if any; or if not, - * constructing and caching it. If key is null, type is constructed but not cached. - */ - private ResolvedType _findOrConstruct(ResolvedTypeKey key, Supplier constructor) - { - ResolvedType type = (key == null) ? null : _resolvedTypes.find(key); + type = (key == null) ? null : _resolvedTypes.find(key); if (type == null) { - type = constructor.get(); - if (key != null) { + type = _constructType(context, rawType, typeBindings); + // [classmate#128]: nor can types with self-references to types still being + // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) + // be cached, whether via type parameters, supertypes or array element types + if (context.typeConstructed(type, typeBindings.typeParameterArray()) + && (key != null)) { _resolvedTypes.put(key, type); } + } else { + context.resolveSelfReferences(type); } return type; } @@ -451,17 +454,7 @@ private ResolvedType _constructType(ClassStack context, Class rawType, TypeBi // (note: [classmate#33] work-around for non-empty bindings of non-generic types // no longer needed as of [classmate#125]: such bindings are never passed) if (typeBindings.isEmpty() && (rawTypeParameters.length > 0)) { - ResolvedType[] types = new ResolvedType[rawTypeParameters.length]; - for (int i = 0; i < rawTypeParameters.length; ++i) { - // Resolve each type parameter to its bound (similar to _fromVariable) - TypeVariable var = rawTypeParameters[i]; - String name = var.getName(); - // Avoid self-reference cycles by marking as unbound during resolution - TypeBindings tempBindings = typeBindings.withUnboundVariable(name); - Type[] bounds = var.getBounds(); - types[i] = _fromAny(context, bounds[0], tempBindings); - } - typeBindings = TypeBindings.create(rawType, types); + typeBindings = TypeBindings.create(rawType, _resolveBounds(context, context, rawType)); } // For other types super interfaces are needed... if (rawType.isInterface()) { @@ -474,6 +467,33 @@ private ResolvedType _constructType(ClassStack context, Class rawType, TypeBi _resolveSuperInterfaces(context, rawType, typeBindings)); } + /** + * Helper method for resolving type parameters of a raw generic type to their bounds. + * + * @param context Resolution context to use + * @param frame Stack frame of the raw type (to mark it as having its bounds resolved) + */ + private ResolvedType[] _resolveBounds(ClassStack context, ClassStack frame, Class rawType) + { + final TypeVariable[] rawTypeParameters = rawType.getTypeParameters(); + final boolean wasResolving = frame.isResolvingBounds(); + frame.setResolvingBounds(true); + try { + ResolvedType[] types = new ResolvedType[rawTypeParameters.length]; + for (int i = 0; i < rawTypeParameters.length; ++i) { + // Resolve each type parameter to its bound (similar to _fromVariable) + TypeVariable var = rawTypeParameters[i]; + // Avoid self-reference cycles by marking as unbound during resolution + TypeBindings tempBindings = TypeBindings.emptyBindings() + .withUnboundVariable(var.getName()); + types[i] = _fromAny(context, var.getBounds()[0], tempBindings); + } + return types; + } finally { + frame.setResolvingBounds(wasResolving); + } + } + private ResolvedType[] _resolveSuperInterfaces(ClassStack context, Class rawType, TypeBindings typeBindings) { Type[] types = rawType.getGenericInterfaces(); @@ -528,7 +548,7 @@ private ResolvedType _fromArrayType(ClassStack context, GenericArrayType arrayTy { // [classmate#125]: bindings only needed for element type, not retained by array ResolvedType elementType = _fromAny(context, arrayType.getGenericComponentType(), typeBindings); - return _arrayOf(_arrayClassFor(elementType), elementType); + return _arrayOf(context, _arrayClassFor(elementType), elementType); } private static Class _arrayClassFor(ResolvedType elementType) { @@ -541,14 +561,25 @@ private static Class _arrayClassFor(ResolvedType elementType) { * element type. Arrays are cached using element type as the "type parameter" * of the key, so that differently parameterized element types do not collide. * Arrays with element types only valid within resolution context (self-references, - * placeholders) are not cached (cache key will be null). + * placeholders, types containing self-references to types still being resolved) + * are not cached. + * + * @param context Resolution context, if any; {@code null} if none */ - private ResolvedArrayType _arrayOf(Class arrayClass, ResolvedType elementType) + private ResolvedArrayType _arrayOf(ClassStack context, Class arrayClass, ResolvedType elementType) { ResolvedTypeKey key = TypeBindings.isContextual(elementType) ? null : _resolvedTypes.key(arrayClass, new ResolvedType[] { elementType }); - return (ResolvedArrayType) _findOrConstruct(key, - () -> new ResolvedArrayType(arrayClass, TypeBindings.emptyBindings(), elementType)); + ResolvedArrayType type = (key == null) ? null : (ResolvedArrayType) _resolvedTypes.find(key); + if (type == null) { + type = new ResolvedArrayType(arrayClass, TypeBindings.emptyBindings(), elementType); + // [classmate#128]: element type may contain self-references to types still being resolved + if (((context == null) || context.containerConstructed(type, elementType)) + && (key != null)) { + _resolvedTypes.put(key, type); + } + } + return type; } private ResolvedType _fromWildcard(ClassStack context, WildcardType wildType, TypeBindings typeBindings) @@ -732,7 +763,7 @@ private ResolvedType _mergeBindings(ResolvedType a, ResolvedType b) if (elem == elemB) { return b; } - return _arrayOf(_arrayClassFor(elem), elem); + return _arrayOf(null, _arrayClassFor(elem), elem); } if (a.getErasedType() != b.getErasedType()) { return null; @@ -786,7 +817,8 @@ private ResolvedType _resolveRawSelfReference(ResolvedType type) /** * Helper method for finding stand-alone type to use in place of given self-reference: * raw one is resolved with type parameters bound to their bounds; others to the type - * referenced. + * referenced (or, if bindings differ from those of the type referenced, resolved + * using own bindings). * * @since 1.8 */ @@ -796,11 +828,43 @@ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) if (raw != selfRef) { return raw; } + // [classmate#128]: referenced type may have different bindings (like `N>` + // within `N`, or raw `GNode` within `GNode`): if so, need to resolve + // with own bindings. Except if self-reference is within type parameters of the + // referenced type (like `E` in raw `Enum>`), since it then + // represents the referenced type itself + final TypeBindings bindings = selfRef.getTypeBindings(); ResolvedType ref = selfRef.getSelfReferencedType(); - if (ref != null) { + if ((ref != null) && (ref.getTypeBindings().equals(bindings) + || _containsType(ref.getTypeBindings(), selfRef))) { return ref; } - return _fromClass(null, selfRef.getErasedType(), selfRef.getTypeBindings()); + final Class erased = selfRef.getErasedType(); + ResolvedType[] params = new ResolvedType[bindings.size()]; + for (int i = 0; i < params.length; ++i) { + params[i] = _resolveSelfReferences(bindings.getBoundType(i), null); + } + return _fromClass(null, erased, TypeBindings.create(erased, params)); + } + + /** + * Helper method for checking whether given bindings contain given type (by identity) + * as type parameter or array element type, at any level of nesting. + * + * @since 1.8 + */ + private static boolean _containsType(TypeBindings bindings, ResolvedType target) + { + for (int i = 0, len = bindings.size(); i < len; ++i) { + ResolvedType t = bindings.getBoundType(i); + while (t.isArray()) { + t = t.getArrayElementType(); + } + if ((t == target) || _containsType(t.getTypeBindings(), target)) { + return true; + } + } + return false; } /** @@ -826,7 +890,7 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List erased, TypeBindings bindings, _elementType = elementType; } + // [classmate#128]: self-reference as element type (like `Node[]` within + // `Node extends Base`) must be equal to fully resolved type @Override public boolean equals(Object other) { return super.equals(other) - && _elementsEqual(_elementType, ((ResolvedArrayType) other)._elementType); - } - - // Self-reference to element type (like `Node[]` within `Node extends Base`) - // must be equal to fully resolved type, so compare erased type and bindings for those - private static boolean _elementsEqual(ResolvedType elem1, ResolvedType elem2) { - if ((elem1 == null) || (elem2 == null)) { - return (elem1 == elem2); - } - if ((elem1 instanceof ResolvedRecursiveType) || (elem2 instanceof ResolvedRecursiveType)) { - return (elem1.getErasedType() == elem2.getErasedType()) - && elem1.getTypeBindings().equals(elem2.getTypeBindings()); - } - return Objects.equals(elem1, elem2); + && _equalTypes(_elementType, ((ResolvedArrayType) other)._elementType); } // NOTE: consistent with `equals()` since hash code of all types (including diff --git a/src/main/java/com/fasterxml/classmate/util/ClassStack.java b/src/main/java/com/fasterxml/classmate/util/ClassStack.java index 57bf73b..499bc43 100644 --- a/src/main/java/com/fasterxml/classmate/util/ClassStack.java +++ b/src/main/java/com/fasterxml/classmate/util/ClassStack.java @@ -1,6 +1,7 @@ package com.fasterxml.classmate.util; import java.util.ArrayList; +import java.util.IdentityHashMap; import com.fasterxml.classmate.ResolvedType; import com.fasterxml.classmate.types.ResolvedRecursiveType; @@ -16,6 +17,46 @@ public final class ClassStack private ArrayList _selfRefs; + /** + * Root of the stack, used for storing state shared by all frames. + * + * @since 1.8 + */ + private final ClassStack _root; + + /** + * Number of frames above this frame (0 for root) + * + * @since 1.8 + */ + private final int _depth; + + /** + * Depth of the shallowest frame that self-references (created during resolution + * of the type this frame represents, including types it contains) point to; + * {@code Integer.MAX_VALUE} if none. + * + * @since 1.8 + */ + private int _minRefDepth = Integer.MAX_VALUE; + + /** + * Whether type parameters of the type this frame represents are being resolved + * to their bounds (to avoid infinite recursion for raw self-references) + * + * @since 1.8 + */ + private boolean _resolvingBounds; + + /** + * Types (shared by all frames, so only used via root) that contain self-references + * to types still being resolved, mapped to depth of the shallowest such frame: + * such types are only valid within their resolution context. + * + * @since 1.8 + */ + private IdentityHashMap _incompleteTypes; + public ClassStack(Class rootType) { this(null, rootType); } @@ -23,6 +64,8 @@ public ClassStack(Class rootType) { private ClassStack(ClassStack parent, Class curr) { _parent = parent; _current = curr; + _root = (parent == null) ? this : parent._root; + _depth = (parent == null) ? 0 : (parent._depth + 1); } /** @@ -69,4 +112,107 @@ public ClassStack find(Class cls) } return null; } + + /* + /********************************************************************** + /* Tracking of incomplete types [classmate#128] + /********************************************************************** + */ + + /** + * Method called (on the innermost frame) when a self-reference to type represented + * by given (enclosing) frame has been created. + * + * @since 1.8 + */ + public void selfReferenceCreated(ResolvedRecursiveType ref, ClassStack target) + { + target.addSelfReference(ref); + _addIncomplete(ref, target._depth); + } + + /** + * Method called (on the innermost frame) when given type, containing given element + * type (like array element type), has been constructed: if element type is + * incomplete, so is the type. + * + * @return True if type is complete (may be cached); false if not + * + * @since 1.8 + */ + public boolean containerConstructed(ResolvedType type, ResolvedType elementType) + { + int depth = _incompleteDepth(elementType); + if (depth == Integer.MAX_VALUE) { + return true; + } + _addIncomplete(type, depth); + return false; + } + + /** + * Method called when type that this stack frame represents has been + * constructed (but not yet cached), with given type parameters. + * Completes self-references to the type (see {@link #resolveSelfReferences}). + * + * @return True if type is complete (may be cached): that is, it does not contain + * self-references to types still being resolved (other than itself); false if not + * + * @since 1.8 + */ + public boolean typeConstructed(ResolvedType type, ResolvedType[] typeParams) + { + resolveSelfReferences(type); + // type parameters were resolved in context of the parent frame, so the parent + // is already aware of their self-references (if any) + int minDepth = _minRefDepth; + for (ResolvedType param : typeParams) { + minDepth = Math.min(minDepth, _incompleteDepth(param)); + } + if (_parent != null) { + _parent._minRefDepth = Math.min(_parent._minRefDepth, _minRefDepth); + } + if (minDepth < _depth) { + _root._incompleteTypes.put(type, minDepth); + return false; + } + return true; + } + + /** + * Accessor for checking whether type parameters of the type this frame represents + * are being resolved to their bounds. + * + * @since 1.8 + */ + public boolean isResolvingBounds() { + return _resolvingBounds; + } + + /** + * @since 1.8 + */ + public void setResolvingBounds(boolean state) { + _resolvingBounds = state; + } + + private void _addIncomplete(ResolvedType type, int depth) + { + _minRefDepth = Math.min(_minRefDepth, depth); + if (_root._incompleteTypes == null) { + _root._incompleteTypes = new IdentityHashMap(); + } + _root._incompleteTypes.put(type, depth); + } + + private int _incompleteDepth(ResolvedType type) + { + if (_root._incompleteTypes != null) { + Integer depth = _root._incompleteTypes.get(type); + if (depth != null) { + return depth.intValue(); + } + } + return Integer.MAX_VALUE; + } } diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java new file mode 100644 index 0000000..1b7b0fe --- /dev/null +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -0,0 +1,170 @@ +package com.fasterxml.classmate; + +import java.util.*; +import java.util.function.Supplier; + +/** + * Tests for [classmate#128]: caching of types with self-references via supertypes, + * and equality of self-references with fully resolved types. + */ +public class TypeResolver128Test extends BaseTest +{ + static class Base { } + + // Self-references via supertypes + static class A extends Base { } + static class B extends Base { } + + static class Mid extends Base { } + static class Outer extends Mid { } + + static class ArrA extends Base { } + static class ArrB extends Base { } + + static class ListA extends Base> { } + static class ListB extends Base { } + + static class SelfComparable implements Comparable, Supplier { + @Override + public int compareTo(SelfComparable o) { return 0; } + @Override + public String get() { return null; } + } + + // Self-references nested within other types + static class LNode extends Base> { } + + static class N extends Base>[]> { } + + // Raw self-reference with raw bound + @SuppressWarnings("rawtypes") + static class RawBound extends Base { } + + /* + /********************************************************************** + /* Caching + /********************************************************************** + */ + + public void testSelfReferenceViaSupertypeNotCached() + { + TypeResolver resolver = new TypeResolver(); + resolver.resolve(A.class); + _verifyFullyResolved(resolver.resolve(B.class).getParentClass() + .getTypeParameters().get(0), A.class); + // but types themselves are cached + assertSame(resolver.resolve(A.class), resolver.resolve(A.class)); + assertSame(resolver.resolve(B.class), resolver.resolve(B.class)); + } + + public void testSelfReferenceViaSuperclassNotCached() + { + TypeResolver resolver = new TypeResolver(); + resolver.resolve(Outer.class); + _verifyFullyResolved(resolver.resolve(Mid.class).getParentClass() + .getTypeParameters().get(0), Outer.class); + } + + public void testSelfReferenceViaSupertypeInArrayNotCached() + { + TypeResolver resolver = new TypeResolver(); + resolver.resolve(ArrA.class); + _verifyFullyResolved(resolver.resolve(ArrB.class).getParentClass() + .getTypeParameters().get(0), ArrA.class); + ResolvedType elem = resolver.resolve(ArrB[].class).getArrayElementType(); + _verifyFullyResolved(elem.getParentClass().getTypeParameters().get(0), ArrA.class); + } + + public void testSelfReferenceViaSupertypeInTypeParameterNotCached() + { + TypeResolver resolver = new TypeResolver(); + resolver.resolve(ListA.class); + ResolvedType listOfB = resolver.resolve(List.class, ListB.class); + ResolvedType b = listOfB.getTypeParameters().get(0); + _verifyFullyResolved(b.getParentClass().getTypeParameters().get(0), ListA.class); + } + + // Types not containing self-references to types being resolved are still cached + public void testTypesWithoutSelfReferencesCached() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType type = resolver.resolve(SelfComparable.class); + ResolvedType supplier = type.findSupertype(Supplier.class); + assertSame(supplier, resolver.resolve(Supplier.class, String.class)); + assertSame(type, resolver.resolve(SelfComparable.class)); + } + + /* + /********************************************************************** + /* Equality + /********************************************************************** + */ + + public void testSelfReferenceInTypeParameterEquality() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType viaParent = resolver.resolve(LNode.class).getParentClass(); + ResolvedType direct = resolver.resolve(Base.class, + resolver.resolve(List.class, LNode.class)); + assertTrue(TypeResolver.isSelfReference(viaParent.getTypeParameters().get(0) + .getTypeParameters().get(0))); + _verifyEqual(direct, viaParent); + } + + public void testNestedSelfReferenceEquality() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType viaParent = resolver.resolve(N.class, String.class).getParentClass() + .getTypeParameters().get(0); + ResolvedType direct = resolver.arrayType(resolver.resolve(N.class, + resolver.resolve(N.class, String.class))); + assertTrue(TypeResolver.isSelfReference(viaParent.getArrayElementType())); + _verifyEqual(direct, viaParent); + + // but not equal to differently parameterized type + ResolvedType other = resolver.arrayType(resolver.resolve(N.class, + resolver.resolve(N.class, Integer.class))); + assertFalse(viaParent.equals(other)); + assertFalse(other.equals(viaParent)); + } + + public void testSelfReferenceResolvedForSubtype() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType viaParent = resolver.resolve(N.class, String.class).getParentClass(); + ResolvedType subtype = resolver.resolveSubtype(viaParent, N.class); + assertEquals(resolver.resolve(N.class, String.class), subtype); + } + + public void testRawSelfReferenceWithRawBound() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType type = resolver.resolve(RawBound.class); + ResolvedType param = type.getParentClass().getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(param)); + assertSame(RawBound.class, param.getErasedType()); + assertEquals(1, param.getTypeParameters().size()); + } + + /* + /********************************************************************** + /* Helper methods + /********************************************************************** + */ + + private void _verifyFullyResolved(ResolvedType type, Class expType) + { + assertSame(expType, type.getErasedType()); + assertFalse(TypeResolver.isSelfReference(type)); + assertNotNull(type.getParentClass()); + assertSame(expType.getSuperclass(), type.getParentClass().getErasedType()); + } + + private void _verifyEqual(ResolvedType t1, ResolvedType t2) + { + assertEquals(t1, t2); + assertEquals(t2, t1); + assertEquals(t1.hashCode(), t2.hashCode()); + assertEquals(1, new HashSet(Arrays.asList(t1, t2)).size()); + } +} diff --git a/src/test/java/com/fasterxml/classmate/types/ResolvedArrayTypeTest.java b/src/test/java/com/fasterxml/classmate/types/ResolvedArrayTypeTest.java index 21b581c..80e124a 100644 --- a/src/test/java/com/fasterxml/classmate/types/ResolvedArrayTypeTest.java +++ b/src/test/java/com/fasterxml/classmate/types/ResolvedArrayTypeTest.java @@ -103,24 +103,29 @@ public void multiDimArrayOfSelfReferenceNotCached() { assertEquals(Base.class, elem.getParentClass().getErasedType()); } - // Raw self-reference has no bindings, so it must not be equal to any parameterization - // (would break transitivity, and with it Sets/Maps); known limitation: also not equal - // to fully resolved raw type (`GNode[]`) + // Raw self-reference must not be equal to any parameterization (would break + // transitivity, and with it Sets/Maps); but [classmate#128] it is equal to fully + // resolved raw type (`GNode[]`) @Test public void arrayOfRawRecursiveTypeEqualityIsTransitive() { TypeResolver resolver = new TypeResolver(); - ResolvedType viaParent = resolver.resolve(GNode.class).getParentClass() + ResolvedType viaParent = resolver.resolve(GNode.class, String.class).getParentClass() .getTypeParameters().get(0); ResolvedType strings = resolver.arrayType(resolver.resolve(GNode.class, String.class)); ResolvedType integers = resolver.arrayType(resolver.resolve(GNode.class, Integer.class)); + ResolvedType raw = resolver.arrayType(resolver.resolve(GNode.class)); assertTrue(TypeResolver.isSelfReference(viaParent.getArrayElementType())); assertFalse(viaParent.equals(strings)); assertFalse(viaParent.equals(integers)); + assertEquals(raw, viaParent); + assertEquals(viaParent, raw); + assertEquals(raw.hashCode(), viaParent.hashCode()); Set types = new HashSet(); types.add(viaParent); types.add(strings); types.add(integers); + types.add(raw); assertEquals(3, types.size()); } From de7ebd790bae8bb3f72b19974b439053bbb5f911 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 13:57:23 -0700 Subject: [PATCH 02/19] More fixes --- VERSION.txt | 4 +- .../com/fasterxml/classmate/ResolvedType.java | 33 +++- .../com/fasterxml/classmate/TypeResolver.java | 105 ++++++++--- .../fasterxml/classmate/util/ClassStack.java | 168 +++++++++++++----- .../classmate/TypeResolver128Test.java | 99 +++++++++++ 5 files changed, 335 insertions(+), 74 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 466ef7e..3774cb0 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -39,7 +39,9 @@ Release notes: to fully resolved types - Types containing self-references to types still being resolved (like `B` in `B extends Base`, when resolving `A extends Base`; via type parameters, - supertypes or array element types) are no longer cached + supertypes or array element types) are no longer cached; nor are types later + resolved using them (f.ex via `MemberResolver`, `arrayType()` or `resolveSubtype()`, + which also re-resolves them) - Self-references (`ResolvedRecursiveType`) within type parameters or array element types are equal to fully resolved types with same erased type and type bindings (at any level of nesting; f.ex `Base>` from `LNode extends Base>`) diff --git a/src/main/java/com/fasterxml/classmate/ResolvedType.java b/src/main/java/com/fasterxml/classmate/ResolvedType.java index 9d0c28f..f72fc08 100644 --- a/src/main/java/com/fasterxml/classmate/ResolvedType.java +++ b/src/main/java/com/fasterxml/classmate/ResolvedType.java @@ -26,19 +26,46 @@ public abstract class ResolvedType * constructors) of this type */ protected final TypeBindings _typeBindings; - + + /** + * Whether this type contains (via type parameters, array element type or supertypes) + * self-references to types it does not itself contain (like {@code B} in + * {@code B extends Base}, when resolved as part of {@code A extends Base}); + * or is such a self-reference. + * Such types are only valid within the resolution of the type that contains them, + * and are never cached by {@link TypeResolver} (nor are types containing them). + * Set by {@link TypeResolver} when type is constructed, before it is exposed. + * + * @since 1.8 + */ + private boolean _incomplete; + /* /********************************************************************** /* Life cycle /********************************************************************** */ - + protected ResolvedType(Class cls, TypeBindings bindings) { _erasedType = cls; _typeBindings = (bindings == null) ? TypeBindings.emptyBindings() : bindings; } - + + /** + * @since 1.8 + */ + void _markIncomplete() { + _incomplete = true; + } + + /** + * @since 1.8 + */ + boolean _isIncomplete() { + return _incomplete; + } + /** * Method that can be used to check if call to {@link TypeResolver#resolveSubtype(ResolvedType, Class)} * may ever succeed; if false, it will fail with an exception, if true, it may succeed. diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 46b907b..c7e3976 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -373,9 +373,7 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin TypeBindings.emptyBindings())); } // Second: recursive reference? - if (context == null) { - context = new ClassStack(rawType); - } else { + if (context != null) { ClassStack prev = context.find(rawType); if (prev != null) { // Self-reference: needs special handling, then... @@ -390,11 +388,10 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin ResolvedRecursiveType selfRef = new ResolvedRecursiveType(rawType, typeBindings); // [classmate#128]: also need to keep track of types containing self-references // (to types still being resolved) to avoid caching them + ((ResolvedType) selfRef)._markIncomplete(); context.selfReferenceCreated(selfRef, prev); return selfRef; } - // no, can just add - context = context.child(rawType); } // If not, already recently resolved? @@ -404,22 +401,68 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // within resolution context) ResolvedTypeKey key = typeBindings.hasContextualTypes() ? null : _resolvedTypes.key(rawType, typeBindings.typeParameterArray()); - type = (key == null) ? null : _resolvedTypes.find(key); - if (type == null) { - type = _constructType(context, rawType, typeBindings); - // [classmate#128]: nor can types with self-references to types still being - // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) - // be cached, whether via type parameters, supertypes or array element types - if (context.typeConstructed(type, typeBindings.typeParameterArray()) - && (key != null)) { + type = _findType(context, key); + if (type != null) { + return type; + } + // If not, need to construct + context = (context == null) ? new ClassStack(rawType) : context.child(rawType); + type = _constructType(context, rawType, typeBindings); + context.resolveSelfReferences(type); + // [classmate#128]: nor can types with self-references to types still being + // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) + // be cached, whether via type parameters, supertypes or array element types + int paramDepth = Integer.MAX_VALUE; + for (ResolvedType param : type.getTypeBindings().typeParameterArray()) { + paramDepth = Math.min(paramDepth, _incompleteDepth(context, param)); + } + if (context.typeConstructed(key, type, paramDepth)) { + if (key != null) { _resolvedTypes.put(key, type); } } else { - context.resolveSelfReferences(type); + type._markIncomplete(); } return type; } + /** + * Helper method for finding cached type with given key, if any; or, failing that, + * incomplete type constructed earlier during current resolution that may be reused. + * + * @since 1.8 + */ + private ResolvedType _findType(ClassStack context, ResolvedTypeKey key) + { + if (key == null) { + return null; + } + ResolvedType type = _resolvedTypes.find(key); + if ((type == null) && (context != null)) { + type = context.findIncomplete(key); + } + return type; + } + + /** + * Helper method for finding depth of the shallowest frame still being resolved that + * given type contains self-references to (see {@link ClassStack#incompleteDepth}). + * + * @param context Resolution context, if any; {@code null} if none + * + * @return {@code Integer.MAX_VALUE} if type is complete; -1 if it is incomplete + * but not valid within given context + * + * @since 1.8 + */ + private static int _incompleteDepth(ClassStack context, ResolvedType type) + { + if (!type._isIncomplete()) { + return Integer.MAX_VALUE; + } + return (context == null) ? -1 : context.incompleteDepth(type); + } + /** * Factory method for resolving given generic type, defined by using sub-class * instance of {@link GenericType} @@ -570,16 +613,23 @@ private ResolvedArrayType _arrayOf(ClassStack context, Class arrayClass, Reso { ResolvedTypeKey key = TypeBindings.isContextual(elementType) ? null : _resolvedTypes.key(arrayClass, new ResolvedType[] { elementType }); - ResolvedArrayType type = (key == null) ? null : (ResolvedArrayType) _resolvedTypes.find(key); + ResolvedType type = _findType(context, key); if (type == null) { type = new ResolvedArrayType(arrayClass, TypeBindings.emptyBindings(), elementType); // [classmate#128]: element type may contain self-references to types still being resolved - if (((context == null) || context.containerConstructed(type, elementType)) - && (key != null)) { - _resolvedTypes.put(key, type); + int depth = _incompleteDepth(context, elementType); + if (depth == Integer.MAX_VALUE) { + if (key != null) { + _resolvedTypes.put(key, type); + } + } else { + type._markIncomplete(); + if (context != null) { + context.containerConstructed(key, type, depth); + } } } - return type; + return (ResolvedArrayType) type; } private ResolvedType _fromWildcard(ClassStack context, WildcardType wildType, TypeBindings typeBindings) @@ -832,10 +882,12 @@ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) // within `N`, or raw `GNode` within `GNode`): if so, need to resolve // with own bindings. Except if self-reference is within type parameters of the // referenced type (like `E` in raw `Enum>`), since it then - // represents the referenced type itself + // represents the referenced type itself. Unless referenced type is itself only + // valid within its resolution context final TypeBindings bindings = selfRef.getTypeBindings(); ResolvedType ref = selfRef.getSelfReferencedType(); - if ((ref != null) && (ref.getTypeBindings().equals(bindings) + if ((ref != null) && !ref._isIncomplete() + && (ref.getTypeBindings().equals(bindings) || _containsType(ref.getTypeBindings(), selfRef))) { return ref; } @@ -873,7 +925,8 @@ private static boolean _containsType(TypeBindings bindings, ResolvedType target) * resolved types. This includes the type itself, as well as ones within type * parameters and array element types, at any level of nesting: except for * self-references to an enclosing type (like {@code E} in {@code Enum>}) - * which are valid as-is. + * which are valid as-is. Types containing such self-references via their supertypes + * (see [classmate#128]) are re-resolved. * * @param enclosing Enclosing types (containing given type as type parameter), used as * a stack (must be restored before returning); {@code null} if none @@ -884,7 +937,7 @@ private static boolean _containsType(TypeBindings bindings, ResolvedType target) */ private ResolvedType _resolveSelfReferences(ResolvedType type, List enclosing) { - if (!TypeBindings.isContextual(type)) { + if (!TypeBindings.isContextual(type) && !type._isIncomplete()) { return type; } if (type.isArray()) { @@ -922,7 +975,11 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List raw = type.getErasedType(); return _fromClass(null, raw, TypeBindings.create(raw, newTypes)); diff --git a/src/main/java/com/fasterxml/classmate/util/ClassStack.java b/src/main/java/com/fasterxml/classmate/util/ClassStack.java index 499bc43..5f5639b 100644 --- a/src/main/java/com/fasterxml/classmate/util/ClassStack.java +++ b/src/main/java/com/fasterxml/classmate/util/ClassStack.java @@ -1,6 +1,7 @@ package com.fasterxml.classmate.util; import java.util.ArrayList; +import java.util.HashMap; import java.util.IdentityHashMap; import com.fasterxml.classmate.ResolvedType; @@ -32,9 +33,10 @@ public final class ClassStack private final int _depth; /** - * Depth of the shallowest frame that self-references (created during resolution - * of the type this frame represents, including types it contains) point to; - * {@code Integer.MAX_VALUE} if none. + * Depth of the shallowest frame that types constructed within this frame (that is, + * as part of the type this frame represents) contain self-references to (directly, + * or via other incomplete types); {@code Integer.MAX_VALUE} if none, and -1 if + * they contain incomplete types from outside of this resolution. * * @since 1.8 */ @@ -49,13 +51,39 @@ public final class ClassStack private boolean _resolvingBounds; /** - * Types (shared by all frames, so only used via root) that contain self-references - * to types still being resolved, mapped to depth of the shallowest such frame: - * such types are only valid within their resolution context. + * Whether the type this frame represents has been constructed (and frame is no + * longer on the stack). * * @since 1.8 */ - private IdentityHashMap _incompleteTypes; + private boolean _completed; + + /** + * For completed frames with incomplete type: shallowest (enclosing) frame that the + * type contains self-references to; {@code null} if type is complete, or contains + * incomplete types from outside of this resolution. + * + * @since 1.8 + */ + private ClassStack _dependency; + + /** + * Incomplete types (see {@link ResolvedType}) constructed during this resolution + * (shared by all frames, so only used via root), mapped to the shallowest frame + * they contain self-references to (when constructed). + * + * @since 1.8 + */ + private IdentityHashMap _incompleteTypes; + + /** + * Incomplete types constructed during this resolution by key (shared by all frames, + * so only used via root): may be reused (but not cached) within this resolution, as + * long as frames they contain self-references to are still being resolved. + * + * @since 1.8 + */ + private HashMap _incompleteByKey; public ClassStack(Class rootType) { this(null, rootType); @@ -128,55 +156,94 @@ public ClassStack find(Class cls) public void selfReferenceCreated(ResolvedRecursiveType ref, ClassStack target) { target.addSelfReference(ref); - _addIncomplete(ref, target._depth); + _dependsOn(target._depth); + _root._addIncomplete(null, ref, target); + } + + /** + * Method called (on the innermost frame) when given incomplete type, containing + * other incomplete type with given depth (see {@link #incompleteDepth}; like array + * element type), has been constructed. + * + * @since 1.8 + */ + public void containerConstructed(ResolvedTypeKey key, ResolvedType type, int depth) + { + _dependsOn(depth); + _root._addIncomplete(key, type, _frameAt(depth)); } /** - * Method called (on the innermost frame) when given type, containing given element - * type (like array element type), has been constructed: if element type is - * incomplete, so is the type. + * Method called when type that this stack frame represents has been + * constructed (but not yet cached), with given depth for its type parameters + * (see {@link #incompleteDepth}). Note that self-references to the type need + * to be completed separately (see {@link #resolveSelfReferences}). * - * @return True if type is complete (may be cached); false if not + * @return True if type is complete (may be cached): that is, it does not contain + * self-references to types still being resolved (other than itself); false if + * not (in which case it is registered as incomplete type with given key) * * @since 1.8 */ - public boolean containerConstructed(ResolvedType type, ResolvedType elementType) + public boolean typeConstructed(ResolvedTypeKey key, ResolvedType type, int paramDepth) { - int depth = _incompleteDepth(elementType); - if (depth == Integer.MAX_VALUE) { + _completed = true; + final int minDepth = Math.min(_minRefDepth, paramDepth); + if (minDepth >= _depth) { return true; } - _addIncomplete(type, depth); + if (_parent != null) { + _parent._dependsOn(minDepth); + } + _dependency = _frameAt(minDepth); + _root._addIncomplete(key, type, _dependency); return false; } /** - * Method called when type that this stack frame represents has been - * constructed (but not yet cached), with given type parameters. - * Completes self-references to the type (see {@link #resolveSelfReferences}). + * Method for finding incomplete type with given key, constructed earlier during + * this resolution, if it may be reused (in the type this frame represents): that is, + * frames it contains self-references to are still being resolved. * - * @return True if type is complete (may be cached): that is, it does not contain - * self-references to types still being resolved (other than itself); false if not + * @return Incomplete type to reuse, if any; {@code null} if none * * @since 1.8 */ - public boolean typeConstructed(ResolvedType type, ResolvedType[] typeParams) + public ResolvedType findIncomplete(ResolvedTypeKey key) { - resolveSelfReferences(type); - // type parameters were resolved in context of the parent frame, so the parent - // is already aware of their self-references (if any) - int minDepth = _minRefDepth; - for (ResolvedType param : typeParams) { - minDepth = Math.min(minDepth, _incompleteDepth(param)); + if ((key == null) || (_root._incompleteByKey == null)) { + return null; } - if (_parent != null) { - _parent._minRefDepth = Math.min(_parent._minRefDepth, _minRefDepth); + ResolvedType type = _root._incompleteByKey.get(key); + if (type != null) { + int depth = incompleteDepth(type); + if (depth >= 0) { + _dependsOn(depth); + return type; + } } - if (minDepth < _depth) { - _root._incompleteTypes.put(type, minDepth); - return false; + return null; + } + + /** + * Accessor for finding depth of the shallowest frame still being resolved that + * given incomplete type contains self-references to (directly, or via types + * already completed). + * + * @return Depth of the frame; or -1 if there is no such frame (type was not + * constructed during this resolution, or only contains self-references to + * completed types), in which case type is not valid within this frame + * + * @since 1.8 + */ + public int incompleteDepth(ResolvedType type) + { + ClassStack frame = (_root._incompleteTypes == null) ? null + : _root._incompleteTypes.get(type); + while ((frame != null) && frame._completed) { + frame = frame._dependency; } - return true; + return (frame == null) ? -1 : frame._depth; } /** @@ -196,23 +263,32 @@ public void setResolvingBounds(boolean state) { _resolvingBounds = state; } - private void _addIncomplete(ResolvedType type, int depth) - { + private void _dependsOn(int depth) { _minRefDepth = Math.min(_minRefDepth, depth); - if (_root._incompleteTypes == null) { - _root._incompleteTypes = new IdentityHashMap(); + } + + /** + * @return Frame with given depth (this frame or one of its ancestors); {@code null} + * for negative depth + */ + private ClassStack _frameAt(int depth) + { + ClassStack frame = (depth < 0) ? null : this; + while ((frame != null) && (frame._depth > depth)) { + frame = frame._parent; } - _root._incompleteTypes.put(type, depth); + return frame; } - private int _incompleteDepth(ResolvedType type) + private void _addIncomplete(ResolvedTypeKey key, ResolvedType type, ClassStack dependency) { - if (_root._incompleteTypes != null) { - Integer depth = _root._incompleteTypes.get(type); - if (depth != null) { - return depth.intValue(); - } + if (_incompleteTypes == null) { + _incompleteTypes = new IdentityHashMap(); + _incompleteByKey = new HashMap(); + } + _incompleteTypes.put(type, dependency); + if (key != null) { + _incompleteByKey.put(key, type); } - return Integer.MAX_VALUE; } } diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index 1b7b0fe..dbe50c0 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -36,6 +36,35 @@ static class LNode extends Base> { } static class N extends Base>[]> { } + // Self-references via supertypes, escaping resolution via members, subtypes + static class FieldBase { + public List values; + public T[] array; + } + static class FA extends FieldBase { } + static class FB extends FieldBase { } + static class FSub extends FieldBase { } + + static class Pair { } + static class SubPair extends Pair { } + static class X extends Base { } + static class Y extends Base { } + static class Z extends Pair> { } + + // Many types referring to each other: incomplete types must be reused + // within resolution (to avoid exponential resolution time) + static class Base10 { } + static class T0 extends Base10 { } + static class T1 extends Base10 { } + static class T2 extends Base10 { } + static class T3 extends Base10 { } + static class T4 extends Base10 { } + static class T5 extends Base10 { } + static class T6 extends Base10 { } + static class T7 extends Base10 { } + static class T8 extends Base10 { } + static class T9 extends Base10 { } + // Raw self-reference with raw bound @SuppressWarnings("rawtypes") static class RawBound extends Base { } @@ -94,6 +123,76 @@ public void testTypesWithoutSelfReferencesCached() assertSame(type, resolver.resolve(SelfComparable.class)); } + // Incomplete types obtained from resolved type must not be cached when used + // for resolving other types later on + public void testIncompleteTypeViaMembersNotCached() + { + TypeResolver resolver = new TypeResolver(); + ResolvedTypeWithMembers members = new MemberResolver(resolver) + .resolve(resolver.resolve(FA.class), null, null); + assertEquals(2, members.getMemberFields().length); + ResolvedType listOfB = resolver.resolve(List.class, FB.class); + _verifyFullyResolved(listOfB.getTypeParameters().get(0).getParentClass() + .getTypeParameters().get(0), FA.class); + ResolvedType arrayOfB = resolver.resolve(FB[].class); + _verifyFullyResolved(arrayOfB.getArrayElementType().getParentClass() + .getTypeParameters().get(0), FA.class); + } + + public void testIncompleteTypeViaArrayTypeNotCached() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType b = resolver.resolve(FA.class).getParentClass().getTypeParameters().get(0); + resolver.arrayType(b); + _verifyFullyResolved(resolver.resolve(FB[].class).getArrayElementType() + .getParentClass().getTypeParameters().get(0), FA.class); + } + + public void testIncompleteTypeViaSubtypeNotCached() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType baseOfB = resolver.resolve(FA.class).getParentClass(); + ResolvedType sub = resolver.resolveSubtype(baseOfB, FSub.class); + // incomplete type itself is not included in subtype either + _verifyFullyResolved(sub.getTypeParameters().get(0).getParentClass() + .getTypeParameters().get(0), FA.class); + _verifyFullyResolved(resolver.resolve(FieldBase.class, FB.class).getTypeParameters() + .get(0).getParentClass().getTypeParameters().get(0), FA.class); + _verifyFullyResolved(resolver.resolve(FSub.class, FB.class).getTypeParameters() + .get(0).getParentClass().getTypeParameters().get(0), FA.class); + } + + public void testIncompleteReferencedTypeViaSubtypeNotCached() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType y = resolver.resolve(X.class).getParentClass().getTypeParameters().get(0); + ResolvedType z = y.getParentClass().getTypeParameters().get(0); + // `Pair>` with self-references to `X` and `Y` + ResolvedType pair = z.getParentClass(); + assertTrue(TypeResolver.isSelfReference(pair.getTypeParameters().get(0))); + resolver.resolveSubtype(pair, SubPair.class); + ResolvedType listOfY = resolver.resolve(List.class, Y.class); + ResolvedType z2 = listOfY.getTypeParameters().get(0).getParentClass() + .getTypeParameters().get(0); + _verifyFullyResolved(z2.getParentClass().getTypeParameters().get(0), X.class); + } + + public void testIncompleteTypesReusedWithinResolution() + { + TypeResolver resolver = new TypeResolver(); + List params = resolver.resolve(T0.class).getParentClass().getTypeParameters(); + assertTrue(TypeResolver.isSelfReference(params.get(0))); + // `T2` first resolved as part of `T1`, then reused + ResolvedType t2 = params.get(2); + assertSame(T2.class, t2.getErasedType()); + assertSame(t2, params.get(1).getParentClass().getTypeParameters().get(2)); + // but not cached + ResolvedType t2Direct = resolver.resolve(T2.class); + assertNotSame(t2, t2Direct); + assertTrue(TypeResolver.isSelfReference(t2Direct.getParentClass().getTypeParameters().get(2))); + assertSame(t2Direct, resolver.resolve(T2.class)); + } + /* /********************************************************************** /* Equality From 2bad003009f366cb577dbfc7a8a94fe282fd1643 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 14:11:15 -0700 Subject: [PATCH 03/19] Add a test --- .../classmate/TypeResolver128Test.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index dbe50c0..fd7a4ad 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -51,6 +51,11 @@ static class X extends Base { } static class Y extends Base { } static class Z extends Pair> { } + // Cycle of 3 types via type parameters + static class C1 extends Pair { } + static class C2 extends Pair { } + static class C3 extends Pair { } + // Many types referring to each other: incomplete types must be reused // within resolution (to avoid exponential resolution time) static class Base10 { } @@ -177,6 +182,24 @@ public void testIncompleteReferencedTypeViaSubtypeNotCached() _verifyFullyResolved(z2.getParentClass().getTypeParameters().get(0), X.class); } + // Subtyping types with self-references to incomplete types, cyclic via + // type parameters, must terminate and produce stand-alone types + public void testSubtypeWithCyclicIncompleteTypes() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType c2 = resolver.resolve(C1.class).getParentClass().getTypeParameters().get(0); + ResolvedType c3 = c2.getParentClass().getTypeParameters().get(0); + // `Pair` with self-references to both + ResolvedType pair = c3.getParentClass(); + assertTrue(TypeResolver.isSelfReference(pair.getTypeParameters().get(0))); + assertTrue(TypeResolver.isSelfReference(pair.getTypeParameters().get(1))); + ResolvedType sub = resolver.resolveSubtype(pair, SubPair.class); + assertSame(SubPair.class, sub.getErasedType()); + _verifyFullyResolved(sub.getTypeParameters().get(0), C1.class); + _verifyFullyResolved(sub.getTypeParameters().get(1), C2.class); + assertEquals(resolver.resolve(SubPair.class, C1.class, C2.class), sub); + } + public void testIncompleteTypesReusedWithinResolution() { TypeResolver resolver = new TypeResolver(); From b314cdb752f3d393f911ca7e41c36386691f56db Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 14:41:57 -0700 Subject: [PATCH 04/19] Improvements --- VERSION.txt | 19 +++---- .../com/fasterxml/classmate/TypeResolver.java | 22 ++++++++- .../types/ResolvedRecursiveType.java | 49 ++++++++++++++++++- .../fasterxml/classmate/util/ClassStack.java | 26 +++++++++- .../classmate/TypeResolver128Test.java | 35 +++++++++++++ 5 files changed, 136 insertions(+), 15 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 3774cb0..0bf5ac3 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -38,17 +38,14 @@ Release notes: #128: Self-references via supertypes still cached; self-references not equal to fully resolved types - Types containing self-references to types still being resolved (like `B` in - `B extends Base`, when resolving `A extends Base`; via type parameters, - supertypes or array element types) are no longer cached; nor are types later - resolved using them (f.ex via `MemberResolver`, `arrayType()` or `resolveSubtype()`, - which also re-resolves them) -- Self-references (`ResolvedRecursiveType`) within type parameters or array element - types are equal to fully resolved types with same erased type and type bindings - (at any level of nesting; f.ex `Base>` from `LNode extends Base>`) -- Raw self-references (like `GNode` in `GNode extends Base`) have their - type parameters resolved to bounds, same as other raw types -- `resolveSubtype()` resolves self-references with bindings different from those - of the referenced type (like `N>` within `N`) using their own bindings + `B extends Base` when resolving `A extends Base`) are no longer cached, + nor are types later resolved using them +- Self-references (`ResolvedRecursiveType`) nested in type parameters or array + element types are equal to fully resolved types with same type bindings +- Raw self-references have type parameters resolved to bounds, like other raw types +- Self-references with bindings different from referenced type (like raw `Mid` + within `Mid`, or `N>` within `N`) now refer to type with own + bindings (`getSelfReferencedType()`, `getParentClass()`, `resolveSubtype()`) (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index c7e3976..0fe187e 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -408,7 +408,12 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // If not, need to construct context = (context == null) ? new ClassStack(rawType) : context.child(rawType); type = _constructType(context, rawType, typeBindings); - context.resolveSelfReferences(type); + // [classmate#128]: self-references with different bindings (like raw `Mid` within + // `Mid`) represent differently parameterized type, resolved lazily. Except + // if within type parameters of the type itself (like `E` in raw `Enum>`) + final ResolvedType resolved = type; + context.resolveSelfReferences(type, ref -> _containsType(resolved.getTypeBindings(), ref) + ? resolved : _standaloneSelfReference(ref)); // [classmate#128]: nor can types with self-references to types still being // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) // be cached, whether via type parameters, supertypes or array element types @@ -891,7 +896,22 @@ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) || _containsType(ref.getTypeBindings(), selfRef))) { return ref; } + return _standaloneSelfReference(selfRef); + } + + /** + * Helper method for resolving stand-alone type that given self-reference represents, + * using its own type bindings (with self-references in them resolved similarly). + * + * @since 1.8 + */ + private ResolvedType _standaloneSelfReference(ResolvedType selfRef) + { final Class erased = selfRef.getErasedType(); + final TypeBindings bindings = selfRef.getTypeBindings(); + if (bindings.isEmpty()) { // raw (or non-generic) type + return _fromClass(null, erased, bindings); + } ResolvedType[] params = new ResolvedType[bindings.size()]; for (int i = 0; i < params.length; ++i) { params[i] = _resolveSelfReferences(bindings.getBoundType(i), null); diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java index efa9e7d..3fbdce9 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java @@ -2,6 +2,7 @@ import java.lang.reflect.Modifier; import java.util.*; +import java.util.function.Supplier; import com.fasterxml.classmate.ResolvedType; import com.fasterxml.classmate.TypeBindings; @@ -24,6 +25,24 @@ public class ResolvedRecursiveType extends ResolvedType */ protected ResolvedType _referencedType; + /** + * For self-references with type bindings different from those of the referenced + * type (like raw {@code Mid} within {@code Mid}, or {@code N>} within + * {@code N}): supplier of the actual type this self-reference represents. + * Resolved lazily since doing so eagerly could lead to infinite recursion. + * + * @since 1.8 + */ + protected Supplier _actualTypeSupplier; + + /** + * Actual type this self-reference represents, if differs from referenced type; + * resolved lazily using {@link #_actualTypeSupplier}. + * + * @since 1.8 + */ + protected volatile ResolvedType _actualType; + /* /********************************************************************** /* Life cycle @@ -49,6 +68,20 @@ public void setReference(ResolvedType ref) _referencedType = ref; } + /** + * Alternative to {@link #setReference(ResolvedType)} used when type bindings of + * this self-reference differ from those of the referenced type: in that case, + * {@link #getSelfReferencedType()} returns type obtained (lazily) from given supplier + * (or referenced type, if equal). + * + * @since 1.8 + */ + public void setReference(ResolvedType ref, Supplier actualType) + { + setReference(ref); + _actualTypeSupplier = actualType; + } + /* /********************************************************************** /* Accessors for related types @@ -64,7 +97,21 @@ public ResolvedType getParentClass() { } @Override - public ResolvedType getSelfReferencedType() { return _referencedType; } + public ResolvedType getSelfReferencedType() { + if (_actualTypeSupplier == null) { + return _referencedType; + } + ResolvedType actual = _actualType; + if (actual == null) { + actual = _actualTypeSupplier.get(); + // retain identity if equal (like for `E` in raw `Enum>`) + if (actual.equals(_referencedType)) { + actual = _referencedType; + } + _actualType = actual; + } + return actual; + } /** * To avoid infinite loops, will return empty list diff --git a/src/main/java/com/fasterxml/classmate/util/ClassStack.java b/src/main/java/com/fasterxml/classmate/util/ClassStack.java index 5f5639b..10fe980 100644 --- a/src/main/java/com/fasterxml/classmate/util/ClassStack.java +++ b/src/main/java/com/fasterxml/classmate/util/ClassStack.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.IdentityHashMap; +import java.util.function.Function; +import java.util.function.Supplier; import com.fasterxml.classmate.ResolvedType; import com.fasterxml.classmate.types.ResolvedRecursiveType; @@ -122,10 +124,30 @@ public void addSelfReference(ResolvedRecursiveType ref) * (if there are any) */ public void resolveSelfReferences(ResolvedType resolved) + { + resolveSelfReferences(resolved, null); + } + + /** + * Method called when type that this stack frame represents is + * fully resolved, allowing self-references to be completed + * (if there are any). Self-references with type bindings different from those + * of the resolved type get the actual type they represent from given function, + * if any (see {@link ResolvedRecursiveType#setReference(ResolvedType, Supplier)}). + * + * @since 1.8 + */ + public void resolveSelfReferences(ResolvedType resolved, + Function actualTypeResolver) { if (_selfRefs != null) { - for (ResolvedRecursiveType ref : _selfRefs) { - ref.setReference(resolved); + for (final ResolvedRecursiveType ref : _selfRefs) { + if ((actualTypeResolver == null) + || ref.getTypeBindings().equals(resolved.getTypeBindings())) { + ref.setReference(resolved); + } else { + ref.setReference(resolved, () -> actualTypeResolver.apply(ref)); + } } } } diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index fd7a4ad..e0bbad2 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -70,6 +70,13 @@ static class T7 extends Base10 { } static class T8 extends Base10 { } static class T9 extends Base10 { } + // Raw self-reference to generic type + @SuppressWarnings("rawtypes") + static class GMid extends Base { + public T value; + } + static class GOuter extends GMid { } + // Raw self-reference with raw bound @SuppressWarnings("rawtypes") static class RawBound extends Base { } @@ -258,6 +265,34 @@ public void testSelfReferenceResolvedForSubtype() assertEquals(resolver.resolve(N.class, String.class), subtype); } + // Raw self-reference must refer to raw type, not to (differently parameterized) + // type being resolved + public void testRawSelfReferenceToGenericType() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType outer = resolver.resolve(GMid.class, String.class).getParentClass() + .getTypeParameters().get(0); + assertSame(GOuter.class, outer.getErasedType()); + ResolvedType parent = outer.getParentClass(); + assertEquals(resolver.resolve(GMid.class, Object.class), parent); + + ResolvedTypeWithMembers members = new MemberResolver(resolver).resolve(outer, null, null); + assertEquals(1, members.getMemberFields().length); + assertSame(Object.class, members.getMemberFields()[0].getType().getErasedType()); + } + + // Self-reference with nested bindings (like `N>` within `N`) must refer + // to type with those bindings + public void testNestedSelfReferenceReferencedType() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType selfRef = resolver.resolve(N.class, String.class).getParentClass() + .getTypeParameters().get(0).getArrayElementType(); + assertTrue(TypeResolver.isSelfReference(selfRef)); + assertEquals(resolver.resolve(N.class, resolver.resolve(N.class, String.class)), + selfRef.getSelfReferencedType()); + } + public void testRawSelfReferenceWithRawBound() { TypeResolver resolver = new TypeResolver(); From 0dfa7dee74ccbfba5de045d9893cd2cce7b1699f Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 14:46:43 -0700 Subject: [PATCH 05/19] Minor code simplification --- .../com/fasterxml/classmate/TypeResolver.java | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 0fe187e..7a83ba2 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -871,28 +871,20 @@ private ResolvedType _resolveRawSelfReference(ResolvedType type) /** * Helper method for finding stand-alone type to use in place of given self-reference: - * raw one is resolved with type parameters bound to their bounds; others to the type - * referenced (or, if bindings differ from those of the type referenced, resolved - * using own bindings). + * the type referenced, if valid outside its resolution context and with matching + * bindings; otherwise resolved using own bindings (raw one to bounds). * * @since 1.8 */ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) { - ResolvedType raw = _resolveRawSelfReference(selfRef); - if (raw != selfRef) { - return raw; - } - // [classmate#128]: referenced type may have different bindings (like `N>` - // within `N`, or raw `GNode` within `GNode`): if so, need to resolve - // with own bindings. Except if self-reference is within type parameters of the - // referenced type (like `E` in raw `Enum>`), since it then - // represents the referenced type itself. Unless referenced type is itself only - // valid within its resolution context - final TypeBindings bindings = selfRef.getTypeBindings(); + // [classmate#128]: referenced type has matching bindings, except if self-reference + // was not constructed by `TypeResolver`; or is within type parameters of the + // referenced type (like `E` in raw `Enum>`), in which case it + // represents the referenced type itself ResolvedType ref = selfRef.getSelfReferencedType(); if ((ref != null) && !ref._isIncomplete() - && (ref.getTypeBindings().equals(bindings) + && (ref.getTypeBindings().equals(selfRef.getTypeBindings()) || _containsType(ref.getTypeBindings(), selfRef))) { return ref; } From 74e3e09dfab16df073e87b45f7b039db3880123f Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 15:03:20 -0700 Subject: [PATCH 06/19] More fixes --- VERSION.txt | 2 +- .../com/fasterxml/classmate/TypeResolver.java | 88 ++++++++++++++----- .../types/ResolvedRecursiveType.java | 37 +++++--- .../classmate/TypeResolver128Test.java | 30 +++++++ 4 files changed, 121 insertions(+), 36 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 0bf5ac3..d651696 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -39,7 +39,7 @@ Release notes: to fully resolved types - Types containing self-references to types still being resolved (like `B` in `B extends Base` when resolving `A extends Base`) are no longer cached, - nor are types later resolved using them + and are re-resolved when used later (like for member types) - Self-references (`ResolvedRecursiveType`) nested in type parameters or array element types are equal to fully resolved types with same type bindings - Raw self-references have type parameters resolved to bounds, like other raw types diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 7a83ba2..2c2b34c 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -198,7 +198,9 @@ public ResolvedArrayType arrayType(Type elementType) */ public ResolvedType resolve(TypeBindings typeBindings, Type jdkType) { - return _fromAny(null, jdkType, typeBindings); + // [classmate#128]: bindings may contain incomplete types (like when resolving + // members of a type), which must not be exposed on their own + return _completeType(_fromAny(null, jdkType, typeBindings)); } /** @@ -372,6 +374,11 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin return _arrayOf(context, rawType, _fromClass(context, rawType.getComponentType(), TypeBindings.emptyBindings())); } + // [classmate#128]: incomplete types (from earlier resolution) must not be + // used as type parameters of new stand-alone types + if (context == null) { + typeBindings = _completeBindings(rawType, typeBindings); + } // Second: recursive reference? if (context != null) { ClassStack prev = context.find(rawType); @@ -431,6 +438,43 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin return type; } + /** + * Helper method for replacing incomplete type (see {@link ResolvedType}) obtained + * from an earlier resolution with stand-alone type, to avoid it being used outside + * of its resolution context. Self-references are retained as-is, however. + * + * @since 1.8 + */ + private ResolvedType _completeType(ResolvedType type) + { + if (type._isIncomplete() && !isSelfReference(type)) { + return _resolveSelfReferences(type, null); + } + return type; + } + + /** + * Helper method for replacing incomplete types within given bindings, if any + * (see {@link #_completeType}). + * + * @since 1.8 + */ + private TypeBindings _completeBindings(Class rawType, TypeBindings bindings) + { + ResolvedType[] types = null; + for (int i = 0, len = bindings.size(); i < len; ++i) { + ResolvedType t = bindings.getBoundType(i); + ResolvedType complete = _completeType(t); + if (complete != t) { + if (types == null) { + types = bindings.typeParameterArray().clone(); + } + types[i] = complete; + } + } + return (types == null) ? bindings : TypeBindings.create(rawType, types); + } + /** * Helper method for finding cached type with given key, if any; or, failing that, * incomplete type constructed earlier during current resolution that may be reused. @@ -616,6 +660,10 @@ private static Class _arrayClassFor(ResolvedType elementType) { */ private ResolvedArrayType _arrayOf(ClassStack context, Class arrayClass, ResolvedType elementType) { + // [classmate#128]: same as with type parameters (see `_fromClass()`) + if (context == null) { + elementType = _completeType(elementType); + } ResolvedTypeKey key = TypeBindings.isContextual(elementType) ? null : _resolvedTypes.key(arrayClass, new ResolvedType[] { elementType }); ResolvedType type = _findType(context, key); @@ -750,7 +798,9 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) exp = _selfReferenceTarget(exp); } else if (isSelfReference(act) && !isSelfReference(exp)) { // but only raw one: others have bindings (possibly with placeholders) to use - act = _resolveRawSelfReference(act); + if (act.getTypeBindings().isEmpty()) { + act = _standaloneSelfReference(act); + } } // [classmate#127]: Array types have no type parameters, so need to verify // (and resolve) element types instead. Must be done before erased type check @@ -793,8 +843,9 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) */ private ResolvedType _mergeBindings(ResolvedType a, ResolvedType b) { - if (a.equals(b)) { - return a; + // [classmate#128]: self-reference equal to fully resolved type, latter preferred + if (ResolvedType._equalTypes(a, b)) { + return isSelfReference(a) ? b : a; } if (_isJavaLangObject(a) && !b.isPrimitive()) { return b; @@ -802,7 +853,17 @@ private ResolvedType _mergeBindings(ResolvedType a, ResolvedType b) if (_isJavaLangObject(b) && !a.isPrimitive()) { return a; } - if (isSelfReference(a) || isSelfReference(b) || (a.isArray() != b.isArray())) { + // [classmate#128]: self-reference (like one in raw `Enum`) can be merged as the + // type it represents; as long as that contains no self-references (to ensure + // termination) + if (isSelfReference(a) != isSelfReference(b)) { + ResolvedType standalone = _standaloneSelfReference(isSelfReference(a) ? a : b); + if (TypeBindings.isContextual(standalone)) { + return null; + } + return isSelfReference(a) ? _mergeBindings(standalone, b) : _mergeBindings(a, standalone); + } + if (isSelfReference(a) || (a.isArray() != b.isArray())) { return null; } if (a.isArray()) { @@ -852,23 +913,6 @@ private static boolean _isJavaLangObject(ResolvedType type) { return type.getErasedType() == Object.class; } - /** - * Helper method for resolving raw self-reference (one without type bindings, for - * generic class) into type with type parameters bound to their bounds; same as - * other raw types are resolved. - * - * @since 1.8 - */ - private ResolvedType _resolveRawSelfReference(ResolvedType type) - { - if (isSelfReference(type) - && type.getTypeBindings().isEmpty() - && type.getErasedType().getTypeParameters().length > 0) { - return _fromClass(null, type.getErasedType(), type.getTypeBindings()); - } - return type; - } - /** * Helper method for finding stand-alone type to use in place of given self-reference: * the type referenced, if valid outside its resolution context and with matching diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java index 3fbdce9..730f125 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java @@ -36,8 +36,8 @@ public class ResolvedRecursiveType extends ResolvedType protected Supplier _actualTypeSupplier; /** - * Actual type this self-reference represents, if differs from referenced type; - * resolved lazily using {@link #_actualTypeSupplier}. + * Actual type this self-reference represents (referenced type, unless bindings + * differ); resolved lazily using {@link #_actualTypeSupplier}, if one given. * * @since 1.8 */ @@ -66,6 +66,7 @@ public void setReference(ResolvedType ref) throw new IllegalStateException("Trying to re-set self reference; old value = "+_referencedType+", new = "+ref); } _referencedType = ref; + _actualType = ref; } /** @@ -79,7 +80,10 @@ public void setReference(ResolvedType ref) public void setReference(ResolvedType ref, Supplier actualType) { setReference(ref); - _actualTypeSupplier = actualType; + synchronized (this) { + _actualTypeSupplier = actualType; + _actualType = null; + } } /* @@ -98,19 +102,26 @@ public ResolvedType getParentClass() { @Override public ResolvedType getSelfReferencedType() { - if (_actualTypeSupplier == null) { - return _referencedType; - } ResolvedType actual = _actualType; - if (actual == null) { - actual = _actualTypeSupplier.get(); - // retain identity if equal (like for `E` in raw `Enum>`) - if (actual.equals(_referencedType)) { - actual = _referencedType; + if (actual != null) { + return actual; + } + synchronized (this) { + if (_actualType == null) { + if (_actualTypeSupplier == null) { + return _referencedType; + } + actual = _actualTypeSupplier.get(); + // retain identity if equal (like for `E` in raw `Enum>`) + if (actual.equals(_referencedType)) { + actual = _referencedType; + } + _actualType = actual; + // no longer needed (and may hold on to `TypeResolver`) + _actualTypeSupplier = null; } - _actualType = actual; + return _actualType; } - return actual; } /** diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index e0bbad2..ab2634d 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -3,6 +3,8 @@ import java.util.*; import java.util.function.Supplier; +import com.fasterxml.classmate.members.ResolvedField; + /** * Tests for [classmate#128]: caching of types with self-references via supertypes, * and equality of self-references with fully resolved types. @@ -46,6 +48,8 @@ static class FB extends FieldBase { } static class FSub extends FieldBase { } static class Pair { } + static class Dup extends Pair { } + enum Color { RED } static class SubPair extends Pair { } static class X extends Base { } static class Y extends Base { } @@ -143,6 +147,18 @@ public void testIncompleteTypeViaMembersNotCached() ResolvedTypeWithMembers members = new MemberResolver(resolver) .resolve(resolver.resolve(FA.class), null, null); assertEquals(2, members.getMemberFields().length); + // member types themselves must not contain incomplete types either + for (ResolvedField field : members.getMemberFields()) { + ResolvedType type = field.getType(); + ResolvedType b = type.isArray() ? type.getArrayElementType() + : type.getTypeParameters().get(0); + _verifyFullyResolved(b.getParentClass().getTypeParameters().get(0), FA.class); + } + // and are cached + ResolvedTypeWithMembers members2 = new MemberResolver(resolver) + .resolve(resolver.resolve(FA.class), null, null); + assertSame(members.getMemberFields()[0].getType(), members2.getMemberFields()[0].getType()); + ResolvedType listOfB = resolver.resolve(List.class, FB.class); _verifyFullyResolved(listOfB.getTypeParameters().get(0).getParentClass() .getTypeParameters().get(0), FA.class); @@ -293,6 +309,20 @@ public void testNestedSelfReferenceReferencedType() selfRef.getSelfReferencedType()); } + // Self-reference (in raw `Enum`) must be merged with compatible type as the type + // it represents + public void testSubtypeMergingSelfReference() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType rawEnum = resolver.resolve(Enum.class); + assertTrue(TypeResolver.isSelfReference(rawEnum.getTypeParameters().get(0))); + ResolvedType enumOfColor = resolver.resolve(Enum.class, + resolver.resolve(Enum.class, Color.class)); + ResolvedType sub = resolver.resolveSubtype( + resolver.resolve(Pair.class, rawEnum, enumOfColor), Dup.class); + assertEquals(resolver.resolve(Dup.class, enumOfColor), sub); + } + public void testRawSelfReferenceWithRawBound() { TypeResolver resolver = new TypeResolver(); From 1f4edd5fd08dc852756a00a96c9624575ff74222 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 15:14:26 -0700 Subject: [PATCH 07/19] Fixes fixes fixes --- .../com/fasterxml/classmate/ResolvedType.java | 2 +- .../com/fasterxml/classmate/TypeResolver.java | 28 ++++++++++------- .../types/ResolvedRecursiveType.java | 25 ++++++++++----- .../fasterxml/classmate/util/ClassStack.java | 18 ++++++----- .../classmate/TypeResolver128Test.java | 31 +++++++++++++++++++ 5 files changed, 75 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/ResolvedType.java b/src/main/java/com/fasterxml/classmate/ResolvedType.java index f72fc08..c037576 100644 --- a/src/main/java/com/fasterxml/classmate/ResolvedType.java +++ b/src/main/java/com/fasterxml/classmate/ResolvedType.java @@ -38,7 +38,7 @@ public abstract class ResolvedType * * @since 1.8 */ - private boolean _incomplete; + private volatile boolean _incomplete; /* /********************************************************************** diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 2c2b34c..b6cb73c 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -146,7 +146,8 @@ public ResolvedType resolve(Type type, Type... typeParameters) } else if (type instanceof ResolvedType) { ResolvedType rt = (ResolvedType) type; if (noParams) { - return rt; + // [classmate#128]: same as `resolve(TypeBindings, Type)` + return _completeType(rt); } bindings = rt.getTypeBindings(); rawBase = rt.getErasedType(); @@ -378,9 +379,7 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // used as type parameters of new stand-alone types if (context == null) { typeBindings = _completeBindings(rawType, typeBindings); - } - // Second: recursive reference? - if (context != null) { + } else { // Second: recursive reference? ClassStack prev = context.find(rawType); if (prev != null) { // Self-reference: needs special handling, then... @@ -418,9 +417,10 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // [classmate#128]: self-references with different bindings (like raw `Mid` within // `Mid`) represent differently parameterized type, resolved lazily. Except // if within type parameters of the type itself (like `E` in raw `Enum>`) - final ResolvedType resolved = type; - context.resolveSelfReferences(type, ref -> _containsType(resolved.getTypeBindings(), ref) - ? resolved : _standaloneSelfReference(ref)); + final TypeBindings resolvedBindings = type.getTypeBindings(); + context.resolveSelfReferences(type, ref -> ref.getTypeBindings().equals(resolvedBindings) + || _containsType(resolvedBindings, ref) + ? null : () -> _standaloneSelfReference(ref)); // [classmate#128]: nor can types with self-references to types still being // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) // be cached, whether via type parameters, supertypes or array element types @@ -441,13 +441,15 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin /** * Helper method for replacing incomplete type (see {@link ResolvedType}) obtained * from an earlier resolution with stand-alone type, to avoid it being used outside - * of its resolution context. Self-references are retained as-is, however. + * of its resolution context. Types with self-references in type parameters (or + * self-references themselves) are retained as-is, however, since they are only + * valid within their enclosing types. * * @since 1.8 */ private ResolvedType _completeType(ResolvedType type) { - if (type._isIncomplete() && !isSelfReference(type)) { + if (type._isIncomplete() && !TypeBindings.isContextual(type)) { return _resolveSelfReferences(type, null); } return type; @@ -1024,15 +1026,17 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List supplier; + synchronized (this) { + if (_actualType != null) { + return _actualType; + } + supplier = _actualTypeSupplier; + if (supplier == null) { + return _referencedType; + } + } + // resolved outside of lock, since resolution may access other self-references; + // concurrent calls may resolve more than once, but only first one is retained + actual = supplier.get(); + // retain identity if equal + if (actual.equals(_referencedType)) { + actual = _referencedType; + } synchronized (this) { if (_actualType == null) { - if (_actualTypeSupplier == null) { - return _referencedType; - } - actual = _actualTypeSupplier.get(); - // retain identity if equal (like for `E` in raw `Enum>`) - if (actual.equals(_referencedType)) { - actual = _referencedType; - } _actualType = actual; // no longer needed (and may hold on to `TypeResolver`) _actualTypeSupplier = null; diff --git a/src/main/java/com/fasterxml/classmate/util/ClassStack.java b/src/main/java/com/fasterxml/classmate/util/ClassStack.java index 10fe980..7a836ff 100644 --- a/src/main/java/com/fasterxml/classmate/util/ClassStack.java +++ b/src/main/java/com/fasterxml/classmate/util/ClassStack.java @@ -131,22 +131,24 @@ public void resolveSelfReferences(ResolvedType resolved) /** * Method called when type that this stack frame represents is * fully resolved, allowing self-references to be completed - * (if there are any). Self-references with type bindings different from those - * of the resolved type get the actual type they represent from given function, - * if any (see {@link ResolvedRecursiveType#setReference(ResolvedType, Supplier)}). + * (if there are any). Self-references that represent a type different from the + * resolved one (like ones with different type bindings) get the actual type they + * represent from supplier given by function (null if self-reference represents the + * resolved type), if any (see {@link ResolvedRecursiveType#setReference(ResolvedType, Supplier)}). * * @since 1.8 */ public void resolveSelfReferences(ResolvedType resolved, - Function actualTypeResolver) + Function> actualTypes) { if (_selfRefs != null) { - for (final ResolvedRecursiveType ref : _selfRefs) { - if ((actualTypeResolver == null) - || ref.getTypeBindings().equals(resolved.getTypeBindings())) { + for (ResolvedRecursiveType ref : _selfRefs) { + Supplier actualType = (actualTypes == null) ? null + : actualTypes.apply(ref); + if (actualType == null) { ref.setReference(resolved); } else { - ref.setReference(resolved, () -> actualTypeResolver.apply(ref)); + ref.setReference(resolved, actualType); } } } diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index ab2634d..c29172d 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -81,6 +81,11 @@ static class GMid extends Base { } static class GOuter extends GMid { } + // Self-reference to enclosing type within bound + static class BMid { } + @SuppressWarnings("rawtypes") + static class BOuter> { } + // Raw self-reference with raw bound @SuppressWarnings("rawtypes") static class RawBound extends Base { } @@ -223,6 +228,32 @@ public void testSubtypeWithCyclicIncompleteTypes() assertEquals(resolver.resolve(SubPair.class, C1.class, C2.class), sub); } + // Incomplete types must be completed by all `resolve()` methods + public void testIncompleteTypeResolvedDirectly() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType b = resolver.resolve(A.class).getParentClass().getTypeParameters().get(0); + ResolvedType direct = resolver.resolve(b); + assertNotSame(b, direct); + _verifyFullyResolved(direct.getParentClass().getTypeParameters().get(0), A.class); + assertSame(resolver.resolve(B.class), direct); + assertSame(direct, resolver.resolve(TypeBindings.emptyBindings(), b)); + } + + // Self-references to enclosing types (within incomplete types) are valid as-is, + // so subtyping to the type itself must not change it + public void testSubtypeRetainsSelfReferenceToEnclosingType() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType outer = resolver.resolve(BOuter.class); + ResolvedType mid = outer.getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(mid.getTypeParameters().get(0))); + ResolvedType sub = resolver.resolveSubtype(outer, BOuter.class); + assertEquals(outer, sub); + assertEquals(sub, outer); + assertSame(sub, resolver.resolveSubtype(outer, BOuter.class)); + } + public void testIncompleteTypesReusedWithinResolution() { TypeResolver resolver = new TypeResolver(); From e551acb7ec1c8f0247337ab5a6db77a0bcfb6358 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 15:15:33 -0700 Subject: [PATCH 08/19] VERSION.txt update --- VERSION.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VERSION.txt b/VERSION.txt index d651696..6df3fbc 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -43,9 +43,12 @@ Release notes: - Self-references (`ResolvedRecursiveType`) nested in type parameters or array element types are equal to fully resolved types with same type bindings - Raw self-references have type parameters resolved to bounds, like other raw types + (except within bounds of the type itself, like `T` in `RawBound`) - Self-references with bindings different from referenced type (like raw `Mid` within `Mid`, or `N>` within `N`) now refer to type with own bindings (`getSelfReferencedType()`, `getParentClass()`, `resolveSubtype()`) +- Note: for expanding types (like `N extends Base>[]>`), following + `getSelfReferencedType()` repeatedly yields ever deeper types, never terminating (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) From 73620b9588a06f58da8e8f93bec044421992c619 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 15:44:27 -0700 Subject: [PATCH 09/19] Yet More Fixing --- .../com/fasterxml/classmate/TypeResolver.java | 46 ++++--------------- .../classmate/TypeResolver128Test.java | 22 +++++++++ 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index b6cb73c..fe60c74 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -165,7 +165,8 @@ public ResolvedType resolve(Type type, Type... typeParameters) int len = typeParameters.length; ResolvedType[] resolvedParams = new ResolvedType[len]; for (int i = 0; i < len; ++i) { - resolvedParams[i] = _fromAny(null, typeParameters[i], bindings); + // [classmate#128]: type parameters may be incomplete types (from earlier resolution) + resolvedParams[i] = _completeType(_fromAny(null, typeParameters[i], bindings)); } return _fromClass(null, rawBase, TypeBindings.create(rawBase, resolvedParams)); } @@ -375,11 +376,8 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin return _arrayOf(context, rawType, _fromClass(context, rawType.getComponentType(), TypeBindings.emptyBindings())); } - // [classmate#128]: incomplete types (from earlier resolution) must not be - // used as type parameters of new stand-alone types - if (context == null) { - typeBindings = _completeBindings(rawType, typeBindings); - } else { // Second: recursive reference? + // Second: recursive reference? + if (context != null) { ClassStack prev = context.find(rawType); if (prev != null) { // Self-reference: needs special handling, then... @@ -441,42 +439,19 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin /** * Helper method for replacing incomplete type (see {@link ResolvedType}) obtained * from an earlier resolution with stand-alone type, to avoid it being used outside - * of its resolution context. Types with self-references in type parameters (or - * self-references themselves) are retained as-is, however, since they are only - * valid within their enclosing types. + * of its resolution context (see {@link #_resolveSelfReferences}). Called by public + * entry points only. Self-references themselves are retained as-is, however. * * @since 1.8 */ private ResolvedType _completeType(ResolvedType type) { - if (type._isIncomplete() && !TypeBindings.isContextual(type)) { + if (type._isIncomplete() && !isSelfReference(type)) { return _resolveSelfReferences(type, null); } return type; } - /** - * Helper method for replacing incomplete types within given bindings, if any - * (see {@link #_completeType}). - * - * @since 1.8 - */ - private TypeBindings _completeBindings(Class rawType, TypeBindings bindings) - { - ResolvedType[] types = null; - for (int i = 0, len = bindings.size(); i < len; ++i) { - ResolvedType t = bindings.getBoundType(i); - ResolvedType complete = _completeType(t); - if (complete != t) { - if (types == null) { - types = bindings.typeParameterArray().clone(); - } - types[i] = complete; - } - } - return (types == null) ? bindings : TypeBindings.create(rawType, types); - } - /** * Helper method for finding cached type with given key, if any; or, failing that, * incomplete type constructed earlier during current resolution that may be reused. @@ -662,10 +637,6 @@ private static Class _arrayClassFor(ResolvedType elementType) { */ private ResolvedArrayType _arrayOf(ClassStack context, Class arrayClass, ResolvedType elementType) { - // [classmate#128]: same as with type parameters (see `_fromClass()`) - if (context == null) { - elementType = _completeType(elementType); - } ResolvedTypeKey key = TypeBindings.isContextual(elementType) ? null : _resolvedTypes.key(arrayClass, new ResolvedType[] { elementType }); ResolvedType type = _findType(context, key); @@ -800,7 +771,8 @@ private boolean _verifyAndResolve(ResolvedType exp, ResolvedType act) exp = _selfReferenceTarget(exp); } else if (isSelfReference(act) && !isSelfReference(exp)) { // but only raw one: others have bindings (possibly with placeholders) to use - if (act.getTypeBindings().isEmpty()) { + if (act.getTypeBindings().isEmpty() + && (act.getErasedType().getTypeParameters().length > 0)) { act = _standaloneSelfReference(act); } } diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index c29172d..3648458 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -81,6 +81,13 @@ static class GMid extends Base { } static class GOuter extends GMid { } + // Incomplete type nested along with self-reference + static class HBase { + public T value; + } + static class H extends HBase> { } + static class I extends Base { } + // Self-reference to enclosing type within bound static class BMid { } @SuppressWarnings("rawtypes") @@ -240,6 +247,21 @@ public void testIncompleteTypeResolvedDirectly() assertSame(direct, resolver.resolve(TypeBindings.emptyBindings(), b)); } + // Incomplete types nested within types with self-references must be completed too + public void testIncompleteTypeNestedWithSelfReferenceCompleted() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType h = resolver.resolve(H.class); + ResolvedTypeWithMembers members = new MemberResolver(resolver).resolve(h, null, null); + assertEquals(1, members.getMemberFields().length); + ResolvedType pair = members.getMemberFields()[0].getType(); + assertSame(Pair.class, pair.getErasedType()); + _verifyFullyResolved(pair.getTypeParameters().get(0).getParentClass() + .getTypeParameters().get(0), H.class); + _verifyFullyResolved(pair.getTypeParameters().get(1), H.class); + assertEquals(resolver.resolve(Pair.class, I.class, H.class), pair); + } + // Self-references to enclosing types (within incomplete types) are valid as-is, // so subtyping to the type itself must not change it public void testSubtypeRetainsSelfReferenceToEnclosingType() From 9830084af58c062417c767b0a76a4a487d4684fb Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 15:57:53 -0700 Subject: [PATCH 10/19] Fixes --- .../com/fasterxml/classmate/TypeResolver.java | 51 ++++++++++++------- .../fasterxml/classmate/util/ClassStack.java | 3 ++ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index fe60c74..eb45708 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -3,6 +3,7 @@ import java.io.Serializable; import java.lang.reflect.*; import java.util.*; +import java.util.function.UnaryOperator; import com.fasterxml.classmate.types.*; import com.fasterxml.classmate.util.ClassKey; @@ -922,13 +923,37 @@ private ResolvedType _standaloneSelfReference(ResolvedType selfRef) if (bindings.isEmpty()) { // raw (or non-generic) type return _fromClass(null, erased, bindings); } - ResolvedType[] params = new ResolvedType[bindings.size()]; - for (int i = 0; i < params.length; ++i) { - params[i] = _resolveSelfReferences(bindings.getBoundType(i), null); + ResolvedType[] params = _mapTypes(bindings, t -> _resolveSelfReferences(t, null)); + if (params == null) { + params = bindings.typeParameterArray(); } return _fromClass(null, erased, TypeBindings.create(erased, params)); } + /** + * Helper method for applying given function to types of given bindings. + * + * @return Array of resulting types, if any changed (by identity); {@code null} if none + * + * @since 1.8 + */ + private static ResolvedType[] _mapTypes(TypeBindings bindings, + UnaryOperator mapper) + { + ResolvedType[] types = null; + for (int i = 0, len = bindings.size(); i < len; ++i) { + ResolvedType t = bindings.getBoundType(i); + ResolvedType newT = mapper.apply(t); + if (newT != t) { + if (types == null) { + types = bindings.typeParameterArray().clone(); + } + types[i] = newT; + } + } + return types; + } + /** * Helper method for checking whether given bindings contain given type (by identity) * as type parameter or array element type, at any level of nesting. @@ -987,23 +1012,11 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List(); - } - enclosing.add(type); + final List encl = (enclosing == null) ? new ArrayList<>() : enclosing; + encl.add(type); final TypeBindings bindings = type.getTypeBindings(); - ResolvedType[] newTypes = null; - for (int i = 0, len = bindings.size(); i < len; ++i) { - ResolvedType t = bindings.getBoundType(i); - ResolvedType newT = _resolveSelfReferences(t, enclosing); - if (newT != t) { - if (newTypes == null) { - newTypes = bindings.typeParameterArray().clone(); - } - newTypes[i] = newT; - } - } - enclosing.remove(enclosing.size() - 1); + ResolvedType[] newTypes = _mapTypes(bindings, t -> _resolveSelfReferences(t, encl)); + encl.remove(encl.size() - 1); if (newTypes == null) { // [classmate#128]: type with self-references via supertypes needs to be re-resolved; // but not if it retains self-references to enclosing types (since those are diff --git a/src/main/java/com/fasterxml/classmate/util/ClassStack.java b/src/main/java/com/fasterxml/classmate/util/ClassStack.java index 7a836ff..26f83dc 100644 --- a/src/main/java/com/fasterxml/classmate/util/ClassStack.java +++ b/src/main/java/com/fasterxml/classmate/util/ClassStack.java @@ -122,7 +122,10 @@ public void addSelfReference(ResolvedRecursiveType ref) * Method called when type that this stack frame represents is * fully resolved, allowing self-references to be completed * (if there are any) + * + * @deprecated Since 1.8 use {@link #resolveSelfReferences(ResolvedType, Function)} */ + @Deprecated public void resolveSelfReferences(ResolvedType resolved) { resolveSelfReferences(resolved, null); From a6ad08747c73df2c124b013749f97b3a66856187 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 17:05:44 -0700 Subject: [PATCH 11/19] Fixes (yup) --- VERSION.txt | 10 +-- .../com/fasterxml/classmate/TypeResolver.java | 68 +++++++++++----- .../classmate/types/ResolvedObjectType.java | 4 +- .../types/ResolvedRecursiveType.java | 61 +++++++++----- .../classmate/TypeResolver128Test.java | 80 ++++++++++++++++++- 5 files changed, 174 insertions(+), 49 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 6df3fbc..e431cb8 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -43,12 +43,12 @@ Release notes: - Self-references (`ResolvedRecursiveType`) nested in type parameters or array element types are equal to fully resolved types with same type bindings - Raw self-references have type parameters resolved to bounds, like other raw types - (except within bounds of the type itself, like `T` in `RawBound`) + (except ones created while resolving bounds of the type itself, like `T` in + `RawBound`; types containing those may also be shared) - Self-references with bindings different from referenced type (like raw `Mid` - within `Mid`, or `N>` within `N`) now refer to type with own - bindings (`getSelfReferencedType()`, `getParentClass()`, `resolveSubtype()`) -- Note: for expanding types (like `N extends Base>[]>`), following - `getSelfReferencedType()` repeatedly yields ever deeper types, never terminating + within `Mid`, or `N>` within `N`) now represent type with own + bindings: see new `ResolvedRecursiveType.getActualType()` (also used for + `getParentClass()`, members and `resolveSubtype()`) (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index eb45708..d10a853 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -140,7 +140,7 @@ public ResolvedType resolve(Type type, Type... typeParameters) } else if (type instanceof GenericType) { bindings = TypeBindings.emptyBindings(); if (noParams) { - return _fromGenericType(null, (GenericType) type, bindings); + return _completeType(_fromGenericType(null, (GenericType) type, bindings)); } ResolvedType rt = _fromAny(null, type, bindings); rawBase = rt.getErasedType(); @@ -169,7 +169,8 @@ public ResolvedType resolve(Type type, Type... typeParameters) // [classmate#128]: type parameters may be incomplete types (from earlier resolution) resolvedParams[i] = _completeType(_fromAny(null, typeParameters[i], bindings)); } - return _fromClass(null, rawBase, TypeBindings.create(rawBase, resolvedParams)); + // and the type itself may be incomplete due to self-references in type parameters + return _completeType(_fromClass(null, rawBase, TypeBindings.create(rawBase, resolvedParams))); } /** @@ -202,7 +203,7 @@ public ResolvedArrayType arrayType(Type elementType) public ResolvedType resolve(TypeBindings typeBindings, Type jdkType) { // [classmate#128]: bindings may contain incomplete types (like when resolving - // members of a type), which must not be exposed on their own + // members of a type), which need to be completed where possible return _completeType(_fromAny(null, jdkType, typeBindings)); } @@ -416,9 +417,8 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // [classmate#128]: self-references with different bindings (like raw `Mid` within // `Mid`) represent differently parameterized type, resolved lazily. Except // if within type parameters of the type itself (like `E` in raw `Enum>`) - final TypeBindings resolvedBindings = type.getTypeBindings(); - context.resolveSelfReferences(type, ref -> ref.getTypeBindings().equals(resolvedBindings) - || _containsType(resolvedBindings, ref) + final ResolvedType resolved = type; + context.resolveSelfReferences(type, ref -> _representsReferenced(ref, resolved) ? null : () -> _standaloneSelfReference(ref)); // [classmate#128]: nor can types with self-references to types still being // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) @@ -441,7 +441,11 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin * Helper method for replacing incomplete type (see {@link ResolvedType}) obtained * from an earlier resolution with stand-alone type, to avoid it being used outside * of its resolution context (see {@link #_resolveSelfReferences}). Called by public - * entry points only. Self-references themselves are retained as-is, however. + * entry points only. + *

+ * NOTE: self-references themselves are retained as-is, as are incomplete types with + * self-references (in type parameters) to enclosing types, since those are only valid + * within the enclosing types: so returned type may still be incomplete. * * @since 1.8 */ @@ -897,19 +901,37 @@ private static boolean _isJavaLangObject(ResolvedType type) { */ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) { - // [classmate#128]: referenced type has matching bindings, except if self-reference - // was not constructed by `TypeResolver`; or is within type parameters of the - // referenced type (like `E` in raw `Enum>`), in which case it - // represents the referenced type itself - ResolvedType ref = selfRef.getSelfReferencedType(); - if ((ref != null) && !ref._isIncomplete() - && (ref.getTypeBindings().equals(selfRef.getTypeBindings()) - || _containsType(ref.getTypeBindings(), selfRef))) { - return ref; + final ResolvedRecursiveType rrt = (ResolvedRecursiveType) selfRef; + final ResolvedType ref = rrt.getSelfReferencedType(); + if ((ref != null) && !ref._isIncomplete()) { + if (_representsReferenced(rrt, ref)) { + return ref; + } + // [classmate#128]: actual type (with own bindings), if known (only not known + // if self-reference was not constructed by `TypeResolver`) + ResolvedType actual = rrt.getActualType(); + if (actual != ref) { + return actual; + } } return _standaloneSelfReference(selfRef); } + /** + * Helper method for checking whether given self-reference represents the type it + * references: that is, either has same type bindings, or is within type parameters + * of the referenced type (like {@code E} in raw {@code Enum>}). + * + * @since 1.8 + */ + private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType ref) + { + final TypeBindings refBindings = ref.getTypeBindings(); + return refBindings.equals(selfRef.getTypeBindings()) + || _containsType(refBindings, selfRef, + Collections.newSetFromMap(new IdentityHashMap())); + } + /** * Helper method for resolving stand-alone type that given self-reference represents, * using its own type bindings (with self-references in them resolved similarly). @@ -958,16 +980,23 @@ private static ResolvedType[] _mapTypes(TypeBindings bindings, * Helper method for checking whether given bindings contain given type (by identity) * as type parameter or array element type, at any level of nesting. * + * @param visited Types already checked (by identity), to avoid re-checking shared + * types (which could otherwise take exponential time) + * * @since 1.8 */ - private static boolean _containsType(TypeBindings bindings, ResolvedType target) + private static boolean _containsType(TypeBindings bindings, ResolvedType target, + Set visited) { for (int i = 0, len = bindings.size(); i < len; ++i) { ResolvedType t = bindings.getBoundType(i); while (t.isArray()) { t = t.getArrayElementType(); } - if ((t == target) || _containsType(t.getTypeBindings(), target)) { + if (t == target) { + return true; + } + if (visited.add(t) && _containsType(t.getTypeBindings(), target, visited)) { return true; } } @@ -1002,10 +1031,11 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List`) + ResolvedType rt = ((ResolvedRecursiveType) _superClass).getActualType(); if (!(rt instanceof ResolvedObjectType)) { throw new IllegalStateException("Internal error: self-referential parent type (" +_superClass+") does not resolve into proper ResolvedObjectType, but instead to: " diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java index 2c8f822..68b6f7f 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java @@ -36,8 +36,8 @@ public class ResolvedRecursiveType extends ResolvedType protected Supplier _actualTypeSupplier; /** - * Actual type this self-reference represents (referenced type, unless bindings - * differ); resolved lazily using {@link #_actualTypeSupplier}, if one given. + * Actual type this self-reference represents, if differs from referenced type; + * resolved lazily using {@link #_actualTypeSupplier}. * * @since 1.8 */ @@ -56,34 +56,30 @@ public ResolvedRecursiveType(Class erased, TypeBindings bindings) @Override public boolean canCreateSubtypes() { - return _referencedType.canCreateSubtypes(); + return getActualType().canCreateSubtypes(); } public void setReference(ResolvedType ref) { - // sanity check; should not be called multiple times - if (_referencedType != null) { - throw new IllegalStateException("Trying to re-set self reference; old value = "+_referencedType+", new = "+ref); - } - _referencedType = ref; - _actualType = ref; + setReference(ref, null); } /** * Alternative to {@link #setReference(ResolvedType)} used when type bindings of * this self-reference differ from those of the referenced type: in that case, - * {@link #getSelfReferencedType()} returns type obtained (lazily) from given supplier + * {@link #getActualType()} returns type obtained (lazily) from given supplier * (or referenced type, if equal). * * @since 1.8 */ - public void setReference(ResolvedType ref, Supplier actualType) + public synchronized void setReference(ResolvedType ref, Supplier actualType) { - setReference(ref); - synchronized (this) { - _actualTypeSupplier = actualType; - _actualType = null; + // sanity check; should not be called multiple times + if (_referencedType != null) { + throw new IllegalStateException("Trying to re-set self reference; old value = "+_referencedType+", new = "+ref); } + _referencedType = ref; + _actualTypeSupplier = actualType; } /* @@ -100,8 +96,31 @@ public ResolvedType getParentClass() { return null; } + /** + * Accessor for the type being resolved that this self-reference points to. + * Note that type bindings of this self-reference may differ from those of the + * referenced type (like for raw {@code Mid} within {@code Mid}): if so, + * {@link #getActualType()} returns the type self-reference actually represents. + */ @Override - public ResolvedType getSelfReferencedType() { + public ResolvedType getSelfReferencedType() { return _referencedType; } + + /** + * Accessor for the type this self-reference represents: same as + * {@link #getSelfReferencedType()}, unless type bindings of this self-reference + * differ from those of the referenced type (like raw {@code Mid} within + * {@code Mid}, or {@code N>} within {@code N}), in which case + * it is the type with bindings of this self-reference (resolved lazily). + *

+ * NOTE: for expanding types (like {@code N} above), following actual types + * repeatedly yields ever deeper types. + * + * @return Type this self-reference represents; {@code null} if not yet resolved + * + * @since 1.8 + */ + public ResolvedType getActualType() + { ResolvedType actual = _actualType; if (actual != null) { return actual; @@ -174,15 +193,15 @@ public ResolvedType getArrayElementType() { // interfaces are never arrays, so: */ @Override - public List getMemberFields() { return _referencedType.getMemberFields(); } + public List getMemberFields() { return getActualType().getMemberFields(); } @Override - public List getStaticFields() { return _referencedType.getStaticFields(); } + public List getStaticFields() { return getActualType().getStaticFields(); } @Override - public List getStaticMethods() { return _referencedType.getStaticMethods(); } + public List getStaticMethods() { return getActualType().getStaticMethods(); } @Override - public List getMemberMethods() { return _referencedType.getMemberMethods(); } + public List getMemberMethods() { return getActualType().getMemberMethods(); } @Override - public List getConstructors() { return _referencedType.getConstructors(); } + public List getConstructors() { return getActualType().getConstructors(); } /* /********************************************************************** diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index 3648458..11b287b 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -4,6 +4,7 @@ import java.util.function.Supplier; import com.fasterxml.classmate.members.ResolvedField; +import com.fasterxml.classmate.types.ResolvedRecursiveType; /** * Tests for [classmate#128]: caching of types with self-references via supertypes, @@ -36,7 +37,9 @@ static class SelfComparable implements Comparable, Supplier> { } - static class N extends Base>[]> { } + static class N extends Base>[]> { + public T value; + } // Self-references via supertypes, escaping resolution via members, subtypes static class FieldBase { @@ -93,6 +96,40 @@ static class BMid { } @SuppressWarnings("rawtypes") static class BOuter> { } + // Raw self-reference at the bottom of deeply nested shared type parameters + @SuppressWarnings("rawtypes") + static class L0 extends Base { } + static class L1 extends L0> { } + static class L2 extends L1> { } + static class L3 extends L2> { } + static class L4 extends L3> { } + static class L5 extends L4> { } + static class L6 extends L5> { } + static class L7 extends L6> { } + static class L8 extends L7> { } + static class L9 extends L8> { } + static class L10 extends L9> { } + static class L11 extends L10> { } + static class L12 extends L11> { } + static class L13 extends L12> { } + static class L14 extends L13> { } + static class L15 extends L14> { } + static class L16 extends L15> { } + static class L17 extends L16> { } + static class L18 extends L17> { } + static class L19 extends L18> { } + static class L20 extends L19> { } + static class L21 extends L20> { } + static class L22 extends L21> { } + static class L23 extends L22> { } + static class L24 extends L23> { } + static class L25 extends L24> { } + static class L26 extends L25> { } + static class L27 extends L26> { } + static class L28 extends L27> { } + static class L29 extends L28> { } + static class L30 extends L29> { } + // Raw self-reference with raw bound @SuppressWarnings("rawtypes") static class RawBound extends Base { } @@ -358,8 +395,45 @@ public void testNestedSelfReferenceReferencedType() ResolvedType selfRef = resolver.resolve(N.class, String.class).getParentClass() .getTypeParameters().get(0).getArrayElementType(); assertTrue(TypeResolver.isSelfReference(selfRef)); - assertEquals(resolver.resolve(N.class, resolver.resolve(N.class, String.class)), - selfRef.getSelfReferencedType()); + ResolvedType nOfString = resolver.resolve(N.class, String.class); + // referenced type is the type being resolved (for backwards compatibility)... + assertEquals(nOfString, selfRef.getSelfReferencedType()); + // but actual type has own bindings + ResolvedType actual = ((ResolvedRecursiveType) selfRef).getActualType(); + assertEquals(resolver.resolve(N.class, nOfString), actual); + assertSame(actual, ((ResolvedRecursiveType) selfRef).getActualType()); + + // and members are resolved using own bindings too + ResolvedTypeWithMembers members = new MemberResolver(resolver).resolve(selfRef, null, null); + assertEquals(1, members.getMemberFields().length); + assertEquals(nOfString, members.getMemberFields()[0].getType()); + } + + // Checking for self-references must not take exponential time for shared types + public void testSelfReferenceWithSharedTypeParameters() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType type = resolver.resolve(L30.class, String.class); + ResolvedType l0 = type; + while (l0.getErasedType() != L0.class) { + l0 = l0.getParentClass(); + } + // raw self-reference (within `L0>`) represents raw `L0` + ResolvedType selfRef = l0.getParentClass().getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(selfRef)); + assertEquals(resolver.resolve(L0.class), ((ResolvedRecursiveType) selfRef).getActualType()); + } + + // Results of `resolve()` must be complete, so resolving again returns same type + public void testResolveWithSelfReferenceParameterIdempotent() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType e = resolver.resolve(Enum.class).getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(e)); + ResolvedType listOfE = resolver.resolve(List.class, e); + assertSame(listOfE, resolver.resolve(listOfE)); + // (not cached, since it contains self-reference, but equal) + assertEquals(listOfE, resolver.resolve(List.class, e)); } // Self-reference (in raw `Enum`) must be merged with compatible type as the type From 396780d91abda0223cf6103a96ea0dc1f75ea89e Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 18:07:16 -0700 Subject: [PATCH 12/19] Fixes --- .../com/fasterxml/classmate/TypeResolver.java | 67 ++++++++++++------- .../types/ResolvedRecursiveType.java | 3 +- .../classmate/TypeResolver128Test.java | 8 ++- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index d10a853..ce8cc8b 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -418,7 +418,8 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // `Mid`) represent differently parameterized type, resolved lazily. Except // if within type parameters of the type itself (like `E` in raw `Enum>`) final ResolvedType resolved = type; - context.resolveSelfReferences(type, ref -> _representsReferenced(ref, resolved) + final ContainedTypes contained = new ContainedTypes(resolved.getTypeBindings()); + context.resolveSelfReferences(type, ref -> _representsReferenced(ref, resolved, contained) ? null : () -> _standaloneSelfReference(ref)); // [classmate#128]: nor can types with self-references to types still being // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) @@ -904,7 +905,7 @@ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) final ResolvedRecursiveType rrt = (ResolvedRecursiveType) selfRef; final ResolvedType ref = rrt.getSelfReferencedType(); if ((ref != null) && !ref._isIncomplete()) { - if (_representsReferenced(rrt, ref)) { + if (_representsReferenced(rrt, ref, null)) { return ref; } // [classmate#128]: actual type (with own bindings), if known (only not known @@ -924,14 +925,18 @@ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) * * @since 1.8 */ - private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType ref) + private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType ref, + ContainedTypes containedInRef) { final TypeBindings refBindings = ref.getTypeBindings(); - return refBindings.equals(selfRef.getTypeBindings()) - || _containsType(refBindings, selfRef, - Collections.newSetFromMap(new IdentityHashMap())); + if (refBindings.equals(selfRef.getTypeBindings())) { + return true; + } + if (containedInRef == null) { + containedInRef = new ContainedTypes(refBindings); + } + return containedInRef.contains(selfRef); } - /** * Helper method for resolving stand-alone type that given self-reference represents, * using its own type bindings (with self-references in them resolved similarly). @@ -977,32 +982,42 @@ private static ResolvedType[] _mapTypes(TypeBindings bindings, } /** - * Helper method for checking whether given bindings contain given type (by identity) - * as type parameter or array element type, at any level of nesting. - * - * @param visited Types already checked (by identity), to avoid re-checking shared - * types (which could otherwise take exponential time) + * Helper class for checking whether given bindings contain given types (by identity) + * as type parameters or array element types, at any level of nesting. Contained types + * are collected lazily, once, so that checking multiple types takes a single pass + * (and shared types are only visited once). * * @since 1.8 */ - private static boolean _containsType(TypeBindings bindings, ResolvedType target, - Set visited) + private static final class ContainedTypes { - for (int i = 0, len = bindings.size(); i < len; ++i) { - ResolvedType t = bindings.getBoundType(i); - while (t.isArray()) { - t = t.getArrayElementType(); - } - if (t == target) { - return true; + private final TypeBindings _bindings; + private Set _types; + + ContainedTypes(TypeBindings bindings) { + _bindings = bindings; + } + + boolean contains(ResolvedType type) { + if (_types == null) { + _types = Collections.newSetFromMap(new IdentityHashMap()); + _collect(_bindings); } - if (visited.add(t) && _containsType(t.getTypeBindings(), target, visited)) { - return true; + return _types.contains(type); + } + + private void _collect(TypeBindings bindings) { + for (int i = 0, len = bindings.size(); i < len; ++i) { + ResolvedType t = bindings.getBoundType(i); + while (t.isArray()) { + t = t.getArrayElementType(); + } + if (_types.add(t)) { + _collect(t.getTypeBindings()); + } } } - return false; } - /** * Helper method for replacing self-references ({@link ResolvedRecursiveType}s) * that are not valid outside their original resolution context with stand-alone @@ -1035,7 +1050,7 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List erased, TypeBindings bindings) @Override public boolean canCreateSubtypes() { - return getActualType().canCreateSubtypes(); + // only depends on erased type, so no need to resolve actual type + return _referencedType.canCreateSubtypes(); } public void setReference(ResolvedType ref) diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index 11b287b..c9d901c 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -50,10 +50,14 @@ static class FA extends FieldBase { } static class FB extends FieldBase { } static class FSub extends FieldBase { } - static class Pair { } + static class Pair { } static class Dup extends Pair { } enum Color { RED } - static class SubPair extends Pair { } + + // (note: type parameters must not be named like classes `A`, `B` declared here: + // javac 8 would resolve them as those classes in `extends` clause) + static class SubPair extends Pair { } + static class X extends Base { } static class Y extends Base { } static class Z extends Pair> { } From 61925821a7da860a006d59fc1de7db498d1cac97 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 18:17:31 -0700 Subject: [PATCH 13/19] ... --- VERSION.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/VERSION.txt b/VERSION.txt index e431cb8..3dcc551 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -42,6 +42,7 @@ Release notes: and are re-resolved when used later (like for member types) - Self-references (`ResolvedRecursiveType`) nested in type parameters or array element types are equal to fully resolved types with same type bindings + (so f.ex raw `Enum` is now equal to `Enum>`) - Raw self-references have type parameters resolved to bounds, like other raw types (except ones created while resolving bounds of the type itself, like `T` in `RawBound`; types containing those may also be shared) From 972ca0f3c637a619252725beb5c69577219e56f0 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 18:42:14 -0700 Subject: [PATCH 14/19] Minor fixes --- .../com/fasterxml/classmate/TypeResolver.java | 39 +++++++++++-------- .../fasterxml/classmate/util/ClassStack.java | 18 +++++---- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index ce8cc8b..51667c1 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -417,10 +417,12 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // [classmate#128]: self-references with different bindings (like raw `Mid` within // `Mid`) represent differently parameterized type, resolved lazily. Except // if within type parameters of the type itself (like `E` in raw `Enum>`) - final ResolvedType resolved = type; - final ContainedTypes contained = new ContainedTypes(resolved.getTypeBindings()); - context.resolveSelfReferences(type, ref -> _representsReferenced(ref, resolved, contained) - ? null : () -> _standaloneSelfReference(ref)); + if (context.hasSelfReferences()) { + final ResolvedType resolved = type; + final ContainedTypes contained = new ContainedTypes(resolved); + context.resolveSelfReferences(type, ref -> _representsReferenced(ref, resolved, contained) + ? null : () -> _standaloneSelfReference(ref)); + } // [classmate#128]: nor can types with self-references to types still being // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) // be cached, whether via type parameters, supertypes or array element types @@ -933,7 +935,7 @@ private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType return true; } if (containedInRef == null) { - containedInRef = new ContainedTypes(refBindings); + containedInRef = new ContainedTypes(ref); } return containedInRef.contains(selfRef); } @@ -982,7 +984,7 @@ private static ResolvedType[] _mapTypes(TypeBindings bindings, } /** - * Helper class for checking whether given bindings contain given types (by identity) + * Helper class for checking whether bindings of given type contain given types (by identity) * as type parameters or array element types, at any level of nesting. Contained types * are collected lazily, once, so that checking multiple types takes a single pass * (and shared types are only visited once). @@ -991,17 +993,21 @@ private static ResolvedType[] _mapTypes(TypeBindings bindings, */ private static final class ContainedTypes { - private final TypeBindings _bindings; + private final ResolvedType _owner; private Set _types; - ContainedTypes(TypeBindings bindings) { - _bindings = bindings; + ContainedTypes(ResolvedType owner) { + _owner = owner; + } + + ResolvedType owner() { + return _owner; } boolean contains(ResolvedType type) { if (_types == null) { _types = Collections.newSetFromMap(new IdentityHashMap()); - _collect(_bindings); + _collect(_owner.getTypeBindings()); } return _types.contains(type); } @@ -1027,14 +1033,15 @@ private void _collect(TypeBindings bindings) { * which are valid as-is. Types containing such self-references via their supertypes * (see [classmate#128]) are re-resolved. * - * @param enclosing Enclosing types (containing given type as type parameter), used as + * @param enclosing Enclosing types (containing given type as type parameter; with + * types they contain, see {@link ContainedTypes}), used as * a stack (must be restored before returning); {@code null} if none * * @return Type with self-references replaced; given type itself if it contains none * * @since 1.8 */ - private ResolvedType _resolveSelfReferences(ResolvedType type, List enclosing) + private ResolvedType _resolveSelfReferences(ResolvedType type, List enclosing) { if (!TypeBindings.isContextual(type) && !type._isIncomplete()) { return type; @@ -1049,16 +1056,16 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List encl = (enclosing == null) ? new ArrayList<>() : enclosing; - encl.add(type); + final List encl = (enclosing == null) ? new ArrayList<>() : enclosing; + encl.add(new ContainedTypes(type)); final TypeBindings bindings = type.getTypeBindings(); ResolvedType[] newTypes = _mapTypes(bindings, t -> _resolveSelfReferences(t, encl)); encl.remove(encl.size() - 1); diff --git a/src/main/java/com/fasterxml/classmate/util/ClassStack.java b/src/main/java/com/fasterxml/classmate/util/ClassStack.java index 26f83dc..b697191 100644 --- a/src/main/java/com/fasterxml/classmate/util/ClassStack.java +++ b/src/main/java/com/fasterxml/classmate/util/ClassStack.java @@ -146,17 +146,21 @@ public void resolveSelfReferences(ResolvedType resolved, { if (_selfRefs != null) { for (ResolvedRecursiveType ref : _selfRefs) { - Supplier actualType = (actualTypes == null) ? null - : actualTypes.apply(ref); - if (actualType == null) { - ref.setReference(resolved); - } else { - ref.setReference(resolved, actualType); - } + ref.setReference(resolved, (actualTypes == null) ? null : actualTypes.apply(ref)); } } } + /** + * @return True if there are self-references to the type this frame represents + * (see {@link #addSelfReference}) + * + * @since 1.8 + */ + public boolean hasSelfReferences() { + return (_selfRefs != null); + } + public ClassStack find(Class cls) { if (_current == cls) return this; From ee44495dc47cdab50f1f9e81a029261df509d429 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 18:59:42 -0700 Subject: [PATCH 15/19] Yet more improvement --- .../com/fasterxml/classmate/TypeResolver.java | 96 +++++++------------ .../classmate/TypeResolver128Test.java | 32 +++++++ 2 files changed, 66 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index 51667c1..fc13368 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -419,8 +419,7 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // if within type parameters of the type itself (like `E` in raw `Enum>`) if (context.hasSelfReferences()) { final ResolvedType resolved = type; - final ContainedTypes contained = new ContainedTypes(resolved); - context.resolveSelfReferences(type, ref -> _representsReferenced(ref, resolved, contained) + context.resolveSelfReferences(type, ref -> _representsReferenced(ref, resolved) ? null : () -> _standaloneSelfReference(ref)); } // [classmate#128]: nor can types with self-references to types still being @@ -907,37 +906,45 @@ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) final ResolvedRecursiveType rrt = (ResolvedRecursiveType) selfRef; final ResolvedType ref = rrt.getSelfReferencedType(); if ((ref != null) && !ref._isIncomplete()) { - if (_representsReferenced(rrt, ref, null)) { - return ref; - } - // [classmate#128]: actual type (with own bindings), if known (only not known - // if self-reference was not constructed by `TypeResolver`) + // [classmate#128]: actual type (with own bindings), if differs ResolvedType actual = rrt.getActualType(); if (actual != ref) { return actual; } + // otherwise referenced type, if self-reference represents it (only not known + // if self-reference was not constructed by `TypeResolver`) + if (_representsReferenced(rrt, ref)) { + return ref; + } } return _standaloneSelfReference(selfRef); } /** * Helper method for checking whether given self-reference represents the type it - * references: that is, either has same type bindings, or is within type parameters + * references: that is, either has same type bindings, or is (directly) a type parameter * of the referenced type (like {@code E} in raw {@code Enum>}). + * Note that self-references nested deeper (like {@code W} within + * {@code W>>}) represent differently parameterized types. * * @since 1.8 */ - private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType ref, - ContainedTypes containedInRef) + private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType ref) { final TypeBindings refBindings = ref.getTypeBindings(); if (refBindings.equals(selfRef.getTypeBindings())) { return true; } - if (containedInRef == null) { - containedInRef = new ContainedTypes(ref); + for (int i = 0, len = refBindings.size(); i < len; ++i) { + ResolvedType t = refBindings.getBoundType(i); + while (t.isArray()) { + t = t.getArrayElementType(); + } + if (t == selfRef) { + return true; + } } - return containedInRef.contains(selfRef); + return false; } /** * Helper method for resolving stand-alone type that given self-reference represents, @@ -983,47 +990,6 @@ private static ResolvedType[] _mapTypes(TypeBindings bindings, return types; } - /** - * Helper class for checking whether bindings of given type contain given types (by identity) - * as type parameters or array element types, at any level of nesting. Contained types - * are collected lazily, once, so that checking multiple types takes a single pass - * (and shared types are only visited once). - * - * @since 1.8 - */ - private static final class ContainedTypes - { - private final ResolvedType _owner; - private Set _types; - - ContainedTypes(ResolvedType owner) { - _owner = owner; - } - - ResolvedType owner() { - return _owner; - } - - boolean contains(ResolvedType type) { - if (_types == null) { - _types = Collections.newSetFromMap(new IdentityHashMap()); - _collect(_owner.getTypeBindings()); - } - return _types.contains(type); - } - - private void _collect(TypeBindings bindings) { - for (int i = 0, len = bindings.size(); i < len; ++i) { - ResolvedType t = bindings.getBoundType(i); - while (t.isArray()) { - t = t.getArrayElementType(); - } - if (_types.add(t)) { - _collect(t.getTypeBindings()); - } - } - } - } /** * Helper method for replacing self-references ({@link ResolvedRecursiveType}s) * that are not valid outside their original resolution context with stand-alone @@ -1033,15 +999,14 @@ private void _collect(TypeBindings bindings) { * which are valid as-is. Types containing such self-references via their supertypes * (see [classmate#128]) are re-resolved. * - * @param enclosing Enclosing types (containing given type as type parameter; with - * types they contain, see {@link ContainedTypes}), used as + * @param enclosing Enclosing types (containing given type as type parameter), used as * a stack (must be restored before returning); {@code null} if none * * @return Type with self-references replaced; given type itself if it contains none * * @since 1.8 */ - private ResolvedType _resolveSelfReferences(ResolvedType type, List enclosing) + private ResolvedType _resolveSelfReferences(ResolvedType type, List enclosing) { if (!TypeBindings.isContextual(type) && !type._isIncomplete()) { return type; @@ -1056,16 +1021,16 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List encl = (enclosing == null) ? new ArrayList<>() : enclosing; - encl.add(new ContainedTypes(type)); + final List encl = (enclosing == null) ? new ArrayList<>() : enclosing; + encl.add(type); final TypeBindings bindings = type.getTypeBindings(); ResolvedType[] newTypes = _mapTypes(bindings, t -> _resolveSelfReferences(t, encl)); encl.remove(encl.size() - 1); @@ -1073,9 +1038,16 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List>`) may be replaced with equal stand-alone raw type + ResolvedType rawType = _fromClass(null, type.getErasedType(), + TypeBindings.emptyBindings()); + return rawType.equals(type) ? rawType : type; + } newTypes = bindings.typeParameterArray(); } final Class raw = type.getErasedType(); diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index c9d901c..5284ff0 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -134,6 +134,15 @@ static class L28 extends L27> { } static class L29 extends L28> { } static class L30 extends L29> { } + // Self-reference nested within bound of the type (with different bindings) + static class W>> { + public T value; + } + + // Incomplete F-bounded type + static class FA extends HBase { } + static class FBounded> extends HBase { } + // Raw self-reference with raw bound @SuppressWarnings("rawtypes") static class RawBound extends Base { } @@ -454,6 +463,29 @@ public void testSubtypeMergingSelfReference() assertEquals(resolver.resolve(Dup.class, enumOfColor), sub); } + // Self-reference nested within bounds of a type represents type with own bindings + public void testNestedSelfReferenceInBound() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType w = resolver.resolve(W.class); + ResolvedTypeWithMembers members = new MemberResolver(resolver).resolve(w, null, null); + assertEquals(1, members.getMemberFields().length); + ResolvedType expW = resolver.resolve(W.class, String.class, Object.class); + assertEquals(resolver.resolve(List.class, expW), members.getMemberFields()[0].getType()); + } + + // Incomplete raw F-bounded type is equal to (and replaced by) stand-alone raw type + public void testIncompleteFBoundedTypeCompleted() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType b = resolver.resolve(FA.class).getParentClass().getTypeParameters().get(0); + assertSame(FBounded.class, b.getErasedType()); + ResolvedType direct = resolver.resolve(FBounded.class); + assertEquals(direct, b); + assertSame(direct, resolver.resolve(b)); + _verifyFullyResolved(resolver.resolve(b).getParentClass().getTypeParameters().get(0), FA.class); + } + public void testRawSelfReferenceWithRawBound() { TypeResolver resolver = new TypeResolver(); From bc07d35e59d426411d06dfaab4610979b24ca06b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 19:15:00 -0700 Subject: [PATCH 16/19] ... --- .../com/fasterxml/classmate/TypeResolver128Test.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index 5284ff0..7d6a9f7 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -140,8 +140,9 @@ static class W>> { } // Incomplete F-bounded type - static class FA extends HBase { } - static class FBounded> extends HBase { } + @SuppressWarnings("rawtypes") + static class FBOuter extends HBase { } + static class FBounded> extends HBase { } // Raw self-reference with raw bound @SuppressWarnings("rawtypes") @@ -478,12 +479,12 @@ public void testNestedSelfReferenceInBound() public void testIncompleteFBoundedTypeCompleted() { TypeResolver resolver = new TypeResolver(); - ResolvedType b = resolver.resolve(FA.class).getParentClass().getTypeParameters().get(0); + ResolvedType b = resolver.resolve(FBOuter.class).getParentClass().getTypeParameters().get(0); assertSame(FBounded.class, b.getErasedType()); ResolvedType direct = resolver.resolve(FBounded.class); assertEquals(direct, b); assertSame(direct, resolver.resolve(b)); - _verifyFullyResolved(resolver.resolve(b).getParentClass().getTypeParameters().get(0), FA.class); + _verifyFullyResolved(resolver.resolve(b).getParentClass().getTypeParameters().get(0), FBOuter.class); } public void testRawSelfReferenceWithRawBound() From 6e0bcc5781f963aad9255e8500fda15022da249c Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 19:31:35 -0700 Subject: [PATCH 17/19] Streamlining --- .../com/fasterxml/classmate/TypeResolver.java | 47 ++--- .../fasterxml/classmate/util/ClassStack.java | 168 ++++-------------- 2 files changed, 48 insertions(+), 167 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index fc13368..b9ee6fb 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -425,16 +425,17 @@ private ResolvedType _fromClass(ClassStack context, Class rawType, TypeBindin // [classmate#128]: nor can types with self-references to types still being // resolved (like `B` in `B extends Base`, when resolving `A extends Base`) // be cached, whether via type parameters, supertypes or array element types - int paramDepth = Integer.MAX_VALUE; - for (ResolvedType param : type.getTypeBindings().typeParameterArray()) { - paramDepth = Math.min(paramDepth, _incompleteDepth(context, param)); + // (note: self-references created within this frame, like ones in bounds of + // raw type, are accounted for by frame; but type parameters are resolved outside) + boolean incomplete = context.hasOuterReferences(); + for (ResolvedType param : typeBindings.typeParameterArray()) { + incomplete |= param._isIncomplete(); } - if (context.typeConstructed(key, type, paramDepth)) { - if (key != null) { - _resolvedTypes.put(key, type); - } - } else { + if (incomplete) { type._markIncomplete(); + context.addIncomplete(key, type); + } else if (key != null) { + _resolvedTypes.put(key, type); } return type; } @@ -477,25 +478,6 @@ private ResolvedType _findType(ClassStack context, ResolvedTypeKey key) return type; } - /** - * Helper method for finding depth of the shallowest frame still being resolved that - * given type contains self-references to (see {@link ClassStack#incompleteDepth}). - * - * @param context Resolution context, if any; {@code null} if none - * - * @return {@code Integer.MAX_VALUE} if type is complete; -1 if it is incomplete - * but not valid within given context - * - * @since 1.8 - */ - private static int _incompleteDepth(ClassStack context, ResolvedType type) - { - if (!type._isIncomplete()) { - return Integer.MAX_VALUE; - } - return (context == null) ? -1 : context.incompleteDepth(type); - } - /** * Factory method for resolving given generic type, defined by using sub-class * instance of {@link GenericType} @@ -650,16 +632,13 @@ private ResolvedArrayType _arrayOf(ClassStack context, Class arrayClass, Reso if (type == null) { type = new ResolvedArrayType(arrayClass, TypeBindings.emptyBindings(), elementType); // [classmate#128]: element type may contain self-references to types still being resolved - int depth = _incompleteDepth(context, elementType); - if (depth == Integer.MAX_VALUE) { - if (key != null) { - _resolvedTypes.put(key, type); - } - } else { + if (elementType._isIncomplete()) { type._markIncomplete(); if (context != null) { - context.containerConstructed(key, type, depth); + context.addIncomplete(key, type); } + } else if (key != null) { + _resolvedTypes.put(key, type); } } return (ResolvedArrayType) type; diff --git a/src/main/java/com/fasterxml/classmate/util/ClassStack.java b/src/main/java/com/fasterxml/classmate/util/ClassStack.java index b697191..9a39afe 100644 --- a/src/main/java/com/fasterxml/classmate/util/ClassStack.java +++ b/src/main/java/com/fasterxml/classmate/util/ClassStack.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.function.Function; import java.util.function.Supplier; @@ -28,21 +27,14 @@ public final class ClassStack private final ClassStack _root; /** - * Number of frames above this frame (0 for root) + * Whether types constructed within this frame (as part of the type it represents) + * contain self-references to types outside of it (that is, to types represented by + * enclosing frames): if so, the type is only valid within this resolution, so it + * must not be cached. Never true for the root frame. * * @since 1.8 */ - private final int _depth; - - /** - * Depth of the shallowest frame that types constructed within this frame (that is, - * as part of the type this frame represents) contain self-references to (directly, - * or via other incomplete types); {@code Integer.MAX_VALUE} if none, and -1 if - * they contain incomplete types from outside of this resolution. - * - * @since 1.8 - */ - private int _minRefDepth = Integer.MAX_VALUE; + private boolean _hasOuterReferences; /** * Whether type parameters of the type this frame represents are being resolved @@ -53,39 +45,13 @@ public final class ClassStack private boolean _resolvingBounds; /** - * Whether the type this frame represents has been constructed (and frame is no - * longer on the stack). + * Incomplete types (see {@link ResolvedType}) constructed during this resolution, + * by key (shared by all frames, so only used via root): reused within this resolution + * (since self-references they contain are all resolved by its end), but not cached. * * @since 1.8 */ - private boolean _completed; - - /** - * For completed frames with incomplete type: shallowest (enclosing) frame that the - * type contains self-references to; {@code null} if type is complete, or contains - * incomplete types from outside of this resolution. - * - * @since 1.8 - */ - private ClassStack _dependency; - - /** - * Incomplete types (see {@link ResolvedType}) constructed during this resolution - * (shared by all frames, so only used via root), mapped to the shallowest frame - * they contain self-references to (when constructed). - * - * @since 1.8 - */ - private IdentityHashMap _incompleteTypes; - - /** - * Incomplete types constructed during this resolution by key (shared by all frames, - * so only used via root): may be reused (but not cached) within this resolution, as - * long as frames they contain self-references to are still being resolved. - * - * @since 1.8 - */ - private HashMap _incompleteByKey; + private HashMap _incompleteTypes; public ClassStack(Class rootType) { this(null, rootType); @@ -95,7 +61,6 @@ private ClassStack(ClassStack parent, Class curr) { _parent = parent; _current = curr; _root = (parent == null) ? this : parent._root; - _depth = (parent == null) ? 0 : (parent._depth + 1); } /** @@ -180,61 +145,34 @@ public ClassStack find(Class cls) /** * Method called (on the innermost frame) when a self-reference to type represented - * by given (enclosing) frame has been created. + * by given (enclosing) frame has been created: types represented by frames in between + * (including this one) then contain self-references to types outside of them. * * @since 1.8 */ public void selfReferenceCreated(ResolvedRecursiveType ref, ClassStack target) { target.addSelfReference(ref); - _dependsOn(target._depth); - _root._addIncomplete(null, ref, target); - } - - /** - * Method called (on the innermost frame) when given incomplete type, containing - * other incomplete type with given depth (see {@link #incompleteDepth}; like array - * element type), has been constructed. - * - * @since 1.8 - */ - public void containerConstructed(ResolvedTypeKey key, ResolvedType type, int depth) - { - _dependsOn(depth); - _root._addIncomplete(key, type, _frameAt(depth)); + for (ClassStack frame = this; frame != target; frame = frame._parent) { + frame._hasOuterReferences = true; + } } /** - * Method called when type that this stack frame represents has been - * constructed (but not yet cached), with given depth for its type parameters - * (see {@link #incompleteDepth}). Note that self-references to the type need - * to be completed separately (see {@link #resolveSelfReferences}). - * - * @return True if type is complete (may be cached): that is, it does not contain - * self-references to types still being resolved (other than itself); false if - * not (in which case it is registered as incomplete type with given key) + * @return True if types constructed within this frame contain self-references + * to types outside of it (see {@link #selfReferenceCreated}) * * @since 1.8 */ - public boolean typeConstructed(ResolvedTypeKey key, ResolvedType type, int paramDepth) - { - _completed = true; - final int minDepth = Math.min(_minRefDepth, paramDepth); - if (minDepth >= _depth) { - return true; - } - if (_parent != null) { - _parent._dependsOn(minDepth); - } - _dependency = _frameAt(minDepth); - _root._addIncomplete(key, type, _dependency); - return false; + public boolean hasOuterReferences() { + return _hasOuterReferences; } /** - * Method for finding incomplete type with given key, constructed earlier during - * this resolution, if it may be reused (in the type this frame represents): that is, - * frames it contains self-references to are still being resolved. + * Method for finding incomplete type with given key constructed earlier during this + * resolution, if any. Since it may contain self-references to any types being + * resolved, types represented by this frame and enclosing frames (except for the + * root) are considered to contain self-references to types outside of them. * * @return Incomplete type to reuse, if any; {@code null} if none * @@ -242,39 +180,32 @@ public boolean typeConstructed(ResolvedTypeKey key, ResolvedType type, int param */ public ResolvedType findIncomplete(ResolvedTypeKey key) { - if ((key == null) || (_root._incompleteByKey == null)) { + if ((key == null) || (_root._incompleteTypes == null)) { return null; } - ResolvedType type = _root._incompleteByKey.get(key); + ResolvedType type = _root._incompleteTypes.get(key); if (type != null) { - int depth = incompleteDepth(type); - if (depth >= 0) { - _dependsOn(depth); - return type; + for (ClassStack frame = this; frame != _root; frame = frame._parent) { + frame._hasOuterReferences = true; } } - return null; + return type; } /** - * Accessor for finding depth of the shallowest frame still being resolved that - * given incomplete type contains self-references to (directly, or via types - * already completed). - * - * @return Depth of the frame; or -1 if there is no such frame (type was not - * constructed during this resolution, or only contains self-references to - * completed types), in which case type is not valid within this frame + * Method for registering incomplete type constructed during this resolution, + * so that it may be reused (see {@link #findIncomplete}). * * @since 1.8 */ - public int incompleteDepth(ResolvedType type) + public void addIncomplete(ResolvedTypeKey key, ResolvedType type) { - ClassStack frame = (_root._incompleteTypes == null) ? null - : _root._incompleteTypes.get(type); - while ((frame != null) && frame._completed) { - frame = frame._dependency; + if (key != null) { + if (_root._incompleteTypes == null) { + _root._incompleteTypes = new HashMap(); + } + _root._incompleteTypes.put(key, type); } - return (frame == null) ? -1 : frame._depth; } /** @@ -293,33 +224,4 @@ public boolean isResolvingBounds() { public void setResolvingBounds(boolean state) { _resolvingBounds = state; } - - private void _dependsOn(int depth) { - _minRefDepth = Math.min(_minRefDepth, depth); - } - - /** - * @return Frame with given depth (this frame or one of its ancestors); {@code null} - * for negative depth - */ - private ClassStack _frameAt(int depth) - { - ClassStack frame = (depth < 0) ? null : this; - while ((frame != null) && (frame._depth > depth)) { - frame = frame._parent; - } - return frame; - } - - private void _addIncomplete(ResolvedTypeKey key, ResolvedType type, ClassStack dependency) - { - if (_incompleteTypes == null) { - _incompleteTypes = new IdentityHashMap(); - _incompleteByKey = new HashMap(); - } - _incompleteTypes.put(type, dependency); - if (key != null) { - _incompleteByKey.put(key, type); - } - } } From e2e3ee82abfb273ad5b54b0808d5e5d18bdfd958 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 20:18:56 -0700 Subject: [PATCH 18/19] Fix #132 --- VERSION.txt | 2 + .../fasterxml/classmate/MemberResolver.java | 6 +- .../com/fasterxml/classmate/TypeResolver.java | 80 ++++++++++++++++--- .../classmate/TypeResolver128Test.java | 40 ++++++++++ 4 files changed, 114 insertions(+), 14 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 3dcc551..aaf6197 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -51,6 +51,8 @@ Release notes: bindings: see new `ResolvedRecursiveType.getActualType()` (also used for `getParentClass()`, members and `resolveSubtype()`) (fix by @cowtowncoder w/ Claude code) +#132: `MemberResolver` misses members inherited via self-referential interface + (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/MemberResolver.java b/src/main/java/com/fasterxml/classmate/MemberResolver.java index d9556f0..8dbc3f7 100644 --- a/src/main/java/com/fasterxml/classmate/MemberResolver.java +++ b/src/main/java/com/fasterxml/classmate/MemberResolver.java @@ -113,10 +113,14 @@ public MemberResolver setConstructorFilter(Filter f) { * @param annotationConfig Configuration of annotation types; which ones to include, how to inherit * @param annotationOverrides Definitions of annotation overrides to use, if any (may be null) */ - public ResolvedTypeWithMembers resolve(final ResolvedType mainType, + public ResolvedTypeWithMembers resolve(ResolvedType mainType, AnnotationConfiguration annotationConfig, AnnotationOverrides annotationOverrides) { + // [classmate#128]: type may be one only valid within its original resolution context + // (like one obtained via type parameters of another type), with self-references in + // its supertypes: if so, need to resolve it as stand-alone type + mainType = _typeResolver.resolve(mainType); List types; HashSet seenTypes = new HashSet(); diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index b9ee6fb..c65e8b7 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -899,12 +899,41 @@ private ResolvedType _selfReferenceTarget(ResolvedType selfRef) return _standaloneSelfReference(selfRef); } + /** + * Helper method for checking whether given type has a self-reference to its own + * class as (direct) type parameter, like raw F-bounded types do. + * + * @since 1.8 + */ + private static boolean _hasSelfReferenceParameter(ResolvedType type) + { + final TypeBindings bindings = type.getTypeBindings(); + for (int i = 0, len = bindings.size(); i < len; ++i) { + ResolvedType t = bindings.getBoundType(i); + if (isSelfReference(t) && (t.getErasedType() == type.getErasedType())) { + return true; + } + } + return false; + } + + /** + * @return Stand-alone raw type of given type, if equal to it; otherwise given type + * + * @since 1.8 + */ + private ResolvedType _rawIfEqual(ResolvedType type) + { + ResolvedType rawType = _fromClass(null, type.getErasedType(), TypeBindings.emptyBindings()); + return rawType.equals(type) ? rawType : type; + } + /** * Helper method for checking whether given self-reference represents the type it - * references: that is, either has same type bindings, or is (directly) a type parameter - * of the referenced type (like {@code E} in raw {@code Enum>}). - * Note that self-references nested deeper (like {@code W} within - * {@code W>>}) represent differently parameterized types. + * references: that is, either has same type bindings, or is the type parameter of + * the referenced type, for type variable bound to the type itself (like {@code E} in + * raw {@code Enum>}). Other self-references (like {@code W} + * in {@code W>}) represent differently parameterized types. * * @since 1.8 */ @@ -915,16 +944,42 @@ private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType return true; } for (int i = 0, len = refBindings.size(); i < len; ++i) { - ResolvedType t = refBindings.getBoundType(i); - while (t.isArray()) { - t = t.getArrayElementType(); - } - if (t == selfRef) { - return true; + if (refBindings.getBoundType(i) == selfRef) { + return _isBoundToSelf(ref.getErasedType(), i); } } return false; } + + /** + * Helper method for checking whether type variable with given index of given class is + * bound to the class itself: either raw ({@code T extends Foo}), or parameterized with + * its own type variables ({@code T extends Foo}). + * + * @since 1.8 + */ + private static boolean _isBoundToSelf(Class raw, int index) + { + TypeVariable[] vars = raw.getTypeParameters(); + if (index >= vars.length) { + return false; + } + Type bound = vars[index].getBounds()[0]; + if (bound == raw) { + return true; + } + if (!(bound instanceof ParameterizedType) + || (((ParameterizedType) bound).getRawType() != raw)) { + return false; + } + for (Type arg : ((ParameterizedType) bound).getActualTypeArguments()) { + if (!(arg instanceof TypeVariable) + || (((TypeVariable) arg).getGenericDeclaration() != raw)) { + return false; + } + } + return true; + } /** * Helper method for resolving stand-alone type that given self-reference represents, * using its own type bindings (with self-references in them resolved similarly). @@ -1023,9 +1078,8 @@ private ResolvedType _resolveSelfReferences(ResolvedType type, List>`) may be replaced with equal stand-alone raw type - ResolvedType rawType = _fromClass(null, type.getErasedType(), - TypeBindings.emptyBindings()); - return rawType.equals(type) ? rawType : type; + return _hasSelfReferenceParameter(type) + ? _rawIfEqual(type) : type; } newTypes = bindings.typeParameterArray(); } diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index 7d6a9f7..36f8c2f 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -139,6 +139,17 @@ static class W>> { public T value; } + // Self-reference as type parameter (with different bindings), not F-bounded + static class WD> { + public A a; + } + + // [classmate#132]: members via self-referential interface + interface Top { void top(); } + interface BaseI extends Top { X get(); } + interface MidI extends BaseI { } + interface OuterI extends MidI { } + // Incomplete F-bounded type @SuppressWarnings("rawtypes") static class FBOuter extends HBase { } @@ -475,6 +486,35 @@ public void testNestedSelfReferenceInBound() assertEquals(resolver.resolve(List.class, expW), members.getMemberFields()[0].getType()); } + // Self-reference as type parameter represents the type itself only if bound to it + // (like `E` in `Enum>`), not otherwise + public void testSelfReferenceTypeParameterNotBoundToSelf() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType param = resolver.resolve(WD.class).getTypeParameters().get(1); + assertTrue(TypeResolver.isSelfReference(param)); + ResolvedType exp = resolver.resolve(WD.class, String.class, Object.class); + assertEquals(exp, ((ResolvedRecursiveType) param).getActualType()); + assertEquals(exp, resolver.resolveSubtype(param, WD.class)); + + ResolvedTypeWithMembers members = new MemberResolver(resolver).resolve(param, null, null); + assertEquals(1, members.getMemberFields().length); + assertSame(String.class, members.getMemberFields()[0].getType().getErasedType()); + } + + // [classmate#132]: members inherited via self-referential interface (of type only + // valid within its resolution context) must not be lost + public void testMembersViaSelfReferentialInterface() + { + TypeResolver resolver = new TypeResolver(); + MemberResolver mr = new MemberResolver(resolver); + ResolvedType outerInMid = resolver.resolve(MidI.class, String.class) + .getImplementedInterfaces().get(0).getTypeParameters().get(0); + assertSame(OuterI.class, outerInMid.getErasedType()); + assertEquals(2, mr.resolve(outerInMid, null, null).getMemberMethods().length); + assertEquals(2, mr.resolve(resolver.resolve(OuterI.class), null, null).getMemberMethods().length); + } + // Incomplete raw F-bounded type is equal to (and replaced by) stand-alone raw type public void testIncompleteFBoundedTypeCompleted() { From eb258f0c62ff382d908eb0bc5230fa02d7d115d0 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 9 Oct 2026 20:41:44 -0700 Subject: [PATCH 19/19] Fixes --- .../fasterxml/classmate/MemberResolver.java | 8 +++ .../com/fasterxml/classmate/TypeResolver.java | 17 ++---- .../classmate/TypeResolver128Test.java | 55 +++++++++++++++++++ 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/MemberResolver.java b/src/main/java/com/fasterxml/classmate/MemberResolver.java index 8dbc3f7..f220be6 100644 --- a/src/main/java/com/fasterxml/classmate/MemberResolver.java +++ b/src/main/java/com/fasterxml/classmate/MemberResolver.java @@ -4,6 +4,7 @@ import java.util.*; import com.fasterxml.classmate.members.*; +import com.fasterxml.classmate.types.ResolvedRecursiveType; import com.fasterxml.classmate.util.ClassKey; /** @@ -120,6 +121,13 @@ public ResolvedTypeWithMembers resolve(ResolvedType mainType, // [classmate#128]: type may be one only valid within its original resolution context // (like one obtained via type parameters of another type), with self-references in // its supertypes: if so, need to resolve it as stand-alone type + // (and self-reference itself has no supertypes, so need to use type it represents) + if (mainType instanceof ResolvedRecursiveType) { + ResolvedType actual = ((ResolvedRecursiveType) mainType).getActualType(); + if (actual != null) { + mainType = actual; + } + } mainType = _typeResolver.resolve(mainType); List types; HashSet seenTypes = new HashSet(); diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index c65e8b7..a41e1a0 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -916,7 +916,6 @@ private static boolean _hasSelfReferenceParameter(ResolvedType type) } return false; } - /** * @return Stand-alone raw type of given type, if equal to it; otherwise given type * @@ -944,8 +943,9 @@ private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType return true; } for (int i = 0, len = refBindings.size(); i < len; ++i) { - if (refBindings.getBoundType(i) == selfRef) { - return _isBoundToSelf(ref.getErasedType(), i); + if ((refBindings.getBoundType(i) == selfRef) + && _isBoundToSelf(ref.getErasedType(), i)) { + return true; } } return false; @@ -954,7 +954,7 @@ private static boolean _representsReferenced(ResolvedType selfRef, ResolvedType /** * Helper method for checking whether type variable with given index of given class is * bound to the class itself: either raw ({@code T extends Foo}), or parameterized with - * its own type variables ({@code T extends Foo}). + * its own type variables, in order ({@code T extends Foo}). * * @since 1.8 */ @@ -972,13 +972,8 @@ private static boolean _isBoundToSelf(Class raw, int index) || (((ParameterizedType) bound).getRawType() != raw)) { return false; } - for (Type arg : ((ParameterizedType) bound).getActualTypeArguments()) { - if (!(arg instanceof TypeVariable) - || (((TypeVariable) arg).getGenericDeclaration() != raw)) { - return false; - } - } - return true; + // must be parameterized with own type variables, in order (like `Foo`) + return Arrays.equals(((ParameterizedType) bound).getActualTypeArguments(), vars); } /** * Helper method for resolving stand-alone type that given self-reference represents, diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java index 36f8c2f..9fb9ae5 100644 --- a/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolver128Test.java @@ -150,6 +150,24 @@ interface BaseI extends Top { X get(); } interface MidI extends BaseI { } interface OuterI extends MidI { } + // Self-reference as main type for member resolution + static class SB { + public int base; + } + static class SR> extends SB { + public T x; + } + + // Bound with own type variables, but not in order + static class PP, U> { + public U u; + } + + // Incomplete F-bounded type, self-reference nested in bound + @SuppressWarnings("rawtypes") + static class NOuter extends HBase { } + static class NFB>> extends HBase { } + // Incomplete F-bounded type @SuppressWarnings("rawtypes") static class FBOuter extends HBase { } @@ -515,6 +533,43 @@ public void testMembersViaSelfReferentialInterface() assertEquals(2, mr.resolve(resolver.resolve(OuterI.class), null, null).getMemberMethods().length); } + // Members of self-reference include inherited ones + public void testMembersOfSelfReference() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType selfRef = resolver.resolve(SR.class).getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(selfRef)); + ResolvedTypeWithMembers members = new MemberResolver(resolver).resolve(selfRef, null, null); + assertEquals(2, members.getMemberFields().length); + } + + // Self-reference represents the type itself only if bound to it with own type + // variables in order (`PP` means `PP` with `U = T`, not raw `PP`) + public void testSelfReferenceWithReorderedBound() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType raw = resolver.resolve(PP.class); + ResolvedType selfRef = raw.getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(selfRef)); + ResolvedType actual = ((ResolvedRecursiveType) selfRef).getActualType(); + assertNotSame(raw, actual); + assertEquals(selfRef.getTypeParameters(), actual.getTypeParameters()); + } + + // Incomplete raw F-bounded type with nested self-reference is completed too + // (nested self-reference representing type with own bindings) + public void testIncompleteNestedFBoundedTypeCompleted() + { + TypeResolver resolver = new TypeResolver(); + ResolvedType b = resolver.resolve(NOuter.class).getParentClass().getTypeParameters().get(0); + assertSame(NFB.class, b.getErasedType()); + assertTrue(b._isIncomplete()); + ResolvedType completed = resolver.resolve(b); + assertFalse(completed._isIncomplete()); + assertEquals(resolver.resolve(NFB.class), completed); + assertSame(completed, resolver.resolve(b)); + } + // Incomplete raw F-bounded type is equal to (and replaced by) stand-alone raw type public void testIncompleteFBoundedTypeCompleted() {