diff --git a/VERSION.txt b/VERSION.txt index b0c4eb2..be2364f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -79,6 +79,15 @@ Release notes: `TypeResolver.isSelfReference()` (or `getSelfReferencedType()`) of member types to detect cycles needs to use other means (like tracking types already visited) (fix by @cowtowncoder w/ Claude code) +#137: Supertype lookups (`findSupertype()`) on a self-reference (`ResolvedRecursiveType`) + return `null` +- `ResolvedRecursiveType.findSupertype()` (and so `typeParametersFor()`) now looks up + supertypes from type self-reference represents (`getActualType()`); `getParentClass()` + and `getImplementedInterfaces()` of self-references still return no supertypes +- NOTE: `findSupertype()` with erased type of self-reference itself now returns the + type it represents, instead of the self-reference (so it is no longer + `ResolvedRecursiveType`) + (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedObjectType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedObjectType.java index afa1981..4ace394 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedObjectType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedObjectType.java @@ -111,12 +111,9 @@ public ResolvedObjectType getParentClass() { if (_superClass == null) { return null; } - if (_superClass instanceof ResolvedObjectType) { - return (ResolvedObjectType) _superClass; - } - // [classmate#128]: actual type, in case bindings differ (like raw `Mid` for `Outer extends Mid` - // within `Mid`) - ResolvedType rt = ((ResolvedRecursiveType) _superClass).getActualType(); + // [classmate#128]: actual type of self-reference, in case bindings differ (like raw + // `Mid` for `Outer extends Mid` within `Mid`) + ResolvedType rt = ResolvedRecursiveType.actualType(_superClass); 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 675620c..5d61411 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java @@ -91,7 +91,9 @@ public synchronized void setReference(ResolvedType ref, Supplier a */ /** - * To avoid infinite loops, will return null; + * To avoid infinite loops (when traversing supertypes and their type parameters), + * will return null; supertypes are accessible using {@link #findSupertype}, or + * via type this self-reference represents (see {@link #getActualType()}). */ @Override public ResolvedType getParentClass() { @@ -153,10 +155,28 @@ public ResolvedType getActualType() } } + /** + * Helper method for replacing given type, if it is a self-reference, with the type + * it represents (see {@link #getActualType()}), since self-references themselves + * expose no supertypes. Self-references not yet resolved (if called during + * resolution), and other types, are returned as-is. + * + * @since 1.8 + */ + static ResolvedType actualType(ResolvedType type) + { + if (type instanceof ResolvedRecursiveType) { + ResolvedType actual = ((ResolvedRecursiveType) type).getActualType(); + if (actual != null) { + return actual; + } + } + return type; + } + /** * Helper method for replacing self-references in given supertypes (like - * implemented interfaces) with types they represent (see {@link #getActualType()}), - * since self-references themselves expose no supertypes. + * implemented interfaces) with types they represent: see {@link #actualType}. * * @since 1.8 */ @@ -167,28 +187,46 @@ static List actualTypes(ResolvedType[] types) } ResolvedType[] result = types; for (int i = 0; i < types.length; ++i) { - if (types[i] instanceof ResolvedRecursiveType) { - ResolvedType actual = ((ResolvedRecursiveType) types[i]).getActualType(); - // may still be unresolved, if called during resolution - if (actual != null) { - if (result == types) { - result = types.clone(); - } - result[i] = actual; + ResolvedType actual = actualType(types[i]); + if (actual != types[i]) { + if (result == types) { + result = types.clone(); } + result[i] = actual; } } return Arrays.asList(result); } /** - * To avoid infinite loops, will return empty list + * To avoid infinite loops (when traversing supertypes and their type parameters), + * will return empty list; supertypes are accessible using {@link #findSupertype}, or + * via type this self-reference represents (see {@link #getActualType()}). */ @Override public List getImplementedInterfaces() { return Collections.emptyList(); } + /** + * Although self-reference itself exposes no supertypes (see {@link #getParentClass()} + * and {@link #getImplementedInterfaces()}), supertypes are looked up from + * the type it represents (see {@link #getActualType()}), including the type itself + * (if given its erased type): this terminates since lookup is by erased type. + * + * @since 1.8 + */ + @Override + public ResolvedType findSupertype(Class erasedSupertype) + { + ResolvedType actual = actualType(this); + // may still be unresolved, if called during resolution + if (actual == this) { + return super.findSupertype(erasedSupertype); + } + return actual.findSupertype(erasedSupertype); + } + /** * To avoid infinite loops, will return null type */ diff --git a/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveType137Test.java b/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveType137Test.java new file mode 100644 index 0000000..155be5b --- /dev/null +++ b/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveType137Test.java @@ -0,0 +1,90 @@ +package com.fasterxml.classmate.types; + +import java.util.List; + +import com.fasterxml.classmate.*; + +/** + * Tests for [classmate#137]: supertype lookups on a self-reference + * ({@link ResolvedRecursiveType}) must give same results as for the type + * it represents. + */ +public class ResolvedRecursiveType137Test extends BaseTest +{ + public interface Top { void top(); } + public interface BaseI extends Top { X get(); } + public interface MidI extends BaseI { } + public interface OuterI extends MidI { } + + public static class Sym> implements Comparable { + @Override + public int compareTo(T o) { return 0; } + } + + // Expanding types + public static class Base { } + public static class C extends Base> { } + public static class D extends C> { } + + private final TypeResolver _typeResolver = new TypeResolver(); + + public void testSupertypesOfSelfReferenceInInterface() + { + ResolvedType outer = _typeResolver.resolve(OuterI.class); + ResolvedType selfRef = outer.findSupertype(BaseI.class).getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(selfRef)); + ResolvedType actual = ((ResolvedRecursiveType) selfRef).getActualType(); + + ResolvedType top = selfRef.findSupertype(Top.class); + assertNotNull(top); + assertEquals(Top.class, top.getErasedType()); + assertEquals(_typeResolver.resolve(OuterI.class).findSupertype(Top.class), top); + + List params = selfRef.typeParametersFor(BaseI.class); + assertNotNull(params); + assertEquals(1, params.size()); + assertEquals(OuterI.class, params.get(0).getErasedType()); + + params = selfRef.typeParametersFor(MidI.class); + assertNotNull(params); + assertEquals(1, params.size()); + assertEquals(String.class, params.get(0).getErasedType()); + + // type itself + assertEquals(actual, selfRef.findSupertype(OuterI.class)); + assertEquals(0, selfRef.typeParametersFor(OuterI.class).size()); + + // and non-supertypes + assertNull(selfRef.findSupertype(Runnable.class)); + assertNull(selfRef.typeParametersFor(String.class)); + } + + public void testSupertypesOfGenericSelfReference() + { + ResolvedType sym = _typeResolver.resolve(Sym.class); + ResolvedType selfRef = sym.getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(selfRef)); + + List params = selfRef.typeParametersFor(Comparable.class); + assertNotNull(params); + assertEquals(1, params.size()); + assertEquals(Sym.class, params.get(0).getErasedType()); + + assertEquals(Object.class, selfRef.findSupertype(Object.class).getErasedType()); + } + + public void testSupertypesOfExpandingSelfReference() + { + // `D` -> `C>` -> `Base>>`, where + // `D>` is a self-reference (`D` being resolved) + ResolvedType d = _typeResolver.resolve(new GenericType>() { }); + ResolvedType selfRef = d.findSupertype(Base.class).getTypeParameters().get(0); + assertTrue(TypeResolver.isSelfReference(selfRef)); + assertEquals(D.class, selfRef.getErasedType()); + + // `D>` -> `C>>` -> `Base>>>` + ResolvedType expC = _typeResolver.resolve(new GenericType>>>() { }); + assertEquals(expC, selfRef.findSupertype(C.class)); + assertEquals(expC.findSupertype(Base.class), selfRef.findSupertype(Base.class)); + } +}