From 6713ab8ca8d2a1b278c0dd5b35b5d9c3ce68599a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 10 Oct 2026 19:33:18 -0700 Subject: [PATCH 1/4] Fix #137 --- VERSION.txt | 6 ++ .../types/ResolvedRecursiveType.java | 19 ++++ .../types/ResolvedRecursiveType137Test.java | 90 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveType137Test.java diff --git a/VERSION.txt b/VERSION.txt index b0c4eb2..0eb33f7 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -79,6 +79,12 @@ 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 + (fix by @cowtowncoder w/ Claude code) 1.7.3 (02-Jan-2026) diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java index 675620c..6f6323a 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java @@ -189,6 +189,25 @@ 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 = getActualType(); + // may still be unresolved, if called during resolution + if (actual == null) { + return (erasedSupertype == _erasedType) ? this : null; + } + 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..3c10cfa --- /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(actual.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() + { + ResolvedType c = _typeResolver.resolve(new GenericType>() { }); + ResolvedType selfRef = c.findSupertype(Base.class).getTypeParameters().get(0); + assertEquals(D.class, selfRef.getErasedType()); + + // `D` -> `C>` -> `Base>>` + ResolvedType base = selfRef.findSupertype(Base.class); + assertNotNull(base); + assertEquals("Base>>", base.getBriefDescription() + .replace(getClass().getName()+"$", "").replace("java.util.", "") + .replace("java.lang.", "")); + assertEquals(C.class, selfRef.findSupertype(C.class).getErasedType()); + } +} From 0261c3490642e5f07f97cc27fecd07968d43f205 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 10 Oct 2026 19:52:11 -0700 Subject: [PATCH 2/4] Minor fix --- .../types/ResolvedRecursiveType.java | 2 +- .../types/ResolvedRecursiveType137Test.java | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java index 6f6323a..356e58a 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java @@ -203,7 +203,7 @@ public ResolvedType findSupertype(Class erasedSupertype) ResolvedType actual = getActualType(); // may still be unresolved, if called during resolution if (actual == null) { - return (erasedSupertype == _erasedType) ? this : null; + return super.findSupertype(erasedSupertype); } return actual.findSupertype(erasedSupertype); } diff --git a/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveType137Test.java b/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveType137Test.java index 3c10cfa..155be5b 100644 --- a/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveType137Test.java +++ b/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveType137Test.java @@ -38,7 +38,7 @@ public void testSupertypesOfSelfReferenceInInterface() ResolvedType top = selfRef.findSupertype(Top.class); assertNotNull(top); assertEquals(Top.class, top.getErasedType()); - assertEquals(actual.findSupertype(Top.class), top); + assertEquals(_typeResolver.resolve(OuterI.class).findSupertype(Top.class), top); List params = selfRef.typeParametersFor(BaseI.class); assertNotNull(params); @@ -75,16 +75,16 @@ public void testSupertypesOfGenericSelfReference() public void testSupertypesOfExpandingSelfReference() { - ResolvedType c = _typeResolver.resolve(new GenericType>() { }); - ResolvedType selfRef = c.findSupertype(Base.class).getTypeParameters().get(0); + // `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 base = selfRef.findSupertype(Base.class); - assertNotNull(base); - assertEquals("Base>>", base.getBriefDescription() - .replace(getClass().getName()+"$", "").replace("java.util.", "") - .replace("java.lang.", "")); - assertEquals(C.class, selfRef.findSupertype(C.class).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)); } } From b464d212682b128b4a25b312e5fdb5a0e4ccc1d5 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 10 Oct 2026 20:05:13 -0700 Subject: [PATCH 3/4] ... --- .../classmate/types/ResolvedObjectType.java | 9 ++--- .../types/ResolvedRecursiveType.java | 39 +++++++++++++------ 2 files changed, 30 insertions(+), 18 deletions(-) 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 356e58a..4cfdf0b 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java @@ -153,10 +153,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,15 +185,12 @@ 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); @@ -200,9 +215,9 @@ public List getImplementedInterfaces() { @Override public ResolvedType findSupertype(Class erasedSupertype) { - ResolvedType actual = getActualType(); + ResolvedType actual = actualType(this); // may still be unresolved, if called during resolution - if (actual == null) { + if (actual == this) { return super.findSupertype(erasedSupertype); } return actual.findSupertype(erasedSupertype); From 3e162b648688c1ff11a73065c002494edceac1ad Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 10 Oct 2026 20:13:05 -0700 Subject: [PATCH 4/4] Doc changes --- VERSION.txt | 3 +++ .../fasterxml/classmate/types/ResolvedRecursiveType.java | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 0eb33f7..be2364f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -84,6 +84,9 @@ Release notes: - `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/ResolvedRecursiveType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java index 4cfdf0b..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() { @@ -197,7 +199,9 @@ static List actualTypes(ResolvedType[] types) } /** - * 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() {