diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f6faee6..2390d8c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "monthly" groups: github-actions: patterns: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5badf09..c64ae0e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,42 +2,47 @@ name: Build on: push: branches: - - master + - main + - "1.6" + - "1.7" paths-ignore: - "README.md" - "release-notes/*" pull_request: branches: - - master + - main + - "1.6" + - "1.7" + permissions: contents: read + jobs: build: - runs-on: 'ubuntu-22.04' + runs-on: 'ubuntu-latest' strategy: fail-fast: false matrix: - # Alas, JDK14 can't be yet used while build is for Java 6 - java_version: ['8', '11'] + java_version: ['8', '17', '21', '25'] env: JAVA_OPTS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" steps: - - uses: actions/checkout@v4.1.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up JDK - uses: actions/setup-java@v3.13.0 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: "temurin" java-version: ${{ matrix.java_version }} cache: 'maven' - name: Build - run: ./mvnw -B -q -ff -ntp verify + run: ./mvnw -B -ff -ntp clean verify - name: Generate code coverage - if: github.event_name != 'pull_request' && matrix.java_version == '8' + if: ${{ github.event_name != 'pull_request' && matrix.java_version == '8' }} run: ./mvnw -B -q -ff -ntp test - name: Publish code coverage - if: github.event_name != 'pull_request' && matrix.java_version == '8' - uses: codecov/codecov-action@v3 + if: ${{ github.event_name != 'pull_request' && matrix.java_version == '8' }} + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ./target/site/jacoco/jacoco.xml + files: ./target/site/jacoco/jacoco.xml flags: unittests diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 8d98d6d..c0bcafe 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,18 +1,3 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/README.md b/README.md index 3c0a34f..0ad578a 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,14 @@ Project is licensed under [Apache 2](http://www.apache.org/licenses/LICENSE-2.0. ## Status -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.fasterxml/classmate/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.fasterxml/classmate/) -[![Javadoc](https://javadoc.io/badge/com.fasterxml/classmate.svg)](http://www.javadoc.io/doc/com.fasterxml/classmate) -[![Tidelift](https://tidelift.com/badges/package/maven/com.fasterxml:classmate)](https://tidelift.com/subscription/pkg/maven-com-fasterxml-classmate?utm_source=maven-com-fasterxml-classmate&utm_medium=referral&utm_campaign=readme) +| Type | Status | +| ---- | ------ | +| Build (CI) | [![Build (github)](https://github.com/FasterXML/java-classmate/actions/workflows/main.yml/badge.svg)](https://github.com/FasterXML/java-classmate/actions/workflows/main.yml) | +| Artifact | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.fasterxml/classmate/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.fasterxml/classmate/) | +| OSS Sponsorship | [![Tidelift](https://tidelift.com/badges/package/maven/com.fasterxml:classmate)](https://tidelift.com/subscription/pkg/maven-com-fasterxml-classmate?utm_source=maven-com-fasterxml-classmate&utm_medium=referral&utm_campaign=readme) | +| JavaDocs | [![Javadoc](https://javadoc.io/badge/com.fasterxml/classmate.svg)](http://www.javadoc.io/doc/com.fasterxml/classmate) | +| Code coverage (master) | [![codecov.io](https://codecov.io/github/FasterXML/java-classmate/coverage.svg?branch=master)](https://codecov.io/github/FasterXML/java-classmate?branch=master) | +| OpenSSF Score | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/FasterXML/java-classmate/badge)](https://securityscorecards.dev/viewer/?uri=github.com/FasterXML/java-classmate) | ## Support @@ -42,6 +47,19 @@ External links that may help include: ----- +## Compatibility + +#### JDK baseline + +ClassMate versions up to 1.6 require Java 6 to run. + +ClassMate versions 1.7 and above require Java 8 to run + +#### JPMS compatibility + +ClassMate versions 1.5 and above contain `module-info.class` information to work with JPMS. +Module name to use is `com.fasterxml.classmate`. + ## Usage ### Maven dependency @@ -52,15 +70,10 @@ To use ClassMate via Maven, include following dependency: com.fasterxml classmate - 1.5.1 + 1.7.3 ``` -### Java 9 module - -Module name to use for Java 9 and above is `com.fasterxml.classmate`; `module-info` included -from version `1.5.0` on. - ### Non-Maven Downloads available from [Project wiki](../../wiki). diff --git a/VERSION.txt b/VERSION.txt index 0be8883..7b7ebda 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -3,6 +3,32 @@ Java ClassMate project: licensed under Apache License 2.0 Release notes: +1.7.3 (02-Jan-2026) + +#117: Regression in 1.7.2 (wrt #53) causes `StackOverflowError` for some recursive types + (reported by Marcin E) + +1.7.2 (27-Dec-2025) + +#53: Allow for subtype resolution with unknown generics + (reported by Carsten W, @CarstenWickner) +- Update to `oss-parent` v74 + +1.7.1 (26-Sep-2025) + +- Branch "master" renamed as "main" +- Update to `oss-parent` v69 to switch to Central Portal publishing + +1.7.0 (02-Jan-2024) + +#51: `TypeResolver.resolve(Long[].class)` should not return a `ResolvedType` + whose parent type is null + (reported by @ljnelson) +#75: Move JDK baseline to Java 8 + (contributed by Dave B, @mebigfatguy) +#83: Simplify code by not calling `Set.contains()` before `add()` +- Remove `Automatic-Module-Name` since we provide proper module-info + 1.6.0 (10-Oct-2023) #70: Remove dead allocation diff --git a/pom.xml b/pom.xml index d0c4727..6a7dc99 100644 --- a/pom.xml +++ b/pom.xml @@ -3,11 +3,11 @@ com.fasterxml oss-parent - 55 + 74 classmate ClassMate - 1.6.1-SNAPSHOT + 1.7.4-SNAPSHOT bundle Library for introspecting types with full generic information including resolving of field and method types. @@ -34,14 +34,12 @@ UTF-8 - 1.6 + 1.8 com.fasterxml.classmate;version=${project.version}, com.fasterxml.classmate.*;version=${project.version} com.fasterxml.classmate.util.* - - com.fasterxml.classmate @@ -69,37 +67,11 @@ com.fasterxml.classmate.*;version=${project.version} - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - sonatype-nexus-staging - https://oss.sonatype.org/ - - b34f19b9cc6224 - - - - + - org.apache.felix - maven-bundle-plugin - - - ${jdk.module.name} - - + org.sonatype.central + central-publishing-maven-plugin - maven-compiler-plugin @@ -111,14 +83,10 @@ com.fasterxml.classmate.*;version=${project.version} org.apache.maven.plugins maven-javadoc-plugin - ${version.plugin.javadoc} ${version.jdk} - ${version.jdk} UTF-8 - - https://docs.oracle.com/javase/8/docs/api/ - + true @@ -153,6 +121,36 @@ com.fasterxml.classmate.*;version=${project.version} + + + + org.jacoco + jacoco-maven-plugin + + + + prepare-agent + + + + + report + test + + report + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/failing/*.java + + + diff --git a/src/main/java/com/fasterxml/classmate/MemberResolver.java b/src/main/java/com/fasterxml/classmate/MemberResolver.java index e12f3c8..d9556f0 100644 --- a/src/main/java/com/fasterxml/classmate/MemberResolver.java +++ b/src/main/java/com/fasterxml/classmate/MemberResolver.java @@ -175,8 +175,7 @@ public ResolvedTypeWithMembers resolve(final ResolvedType mainType, private void _addOverrides(List typesWithOverrides, Set seenTypes, Class override) { ClassKey key = new ClassKey(override); - if (!seenTypes.contains(key)) { - seenTypes.add(key); + if (seenTypes.add(key)) { ResolvedType resolvedOverride = _typeResolver.resolve(override); typesWithOverrides.add(new HierarchicType(resolvedOverride, true, typesWithOverrides.size())); for (ResolvedType r : resolvedOverride.getImplementedInterfaces()) { // interfaces? @@ -194,8 +193,7 @@ private void _addOverrides(List typesWithOverrides, Set raw = override.getErasedType(); if (!_cfgIncludeLangObject && Object.class == raw) return; ClassKey key = new ClassKey(raw); - if (!seenTypes.contains(key)) { - seenTypes.add(key); + if (seenTypes.add(key)) { typesWithOverrides.add(new HierarchicType(override, true, typesWithOverrides.size())); for (ResolvedType r : override.getImplementedInterfaces()) { // interfaces? _addOverrides(typesWithOverrides, seenTypes, r); @@ -227,11 +225,10 @@ protected void _gatherTypes(ResolvedType currentType, Set seenTypes, } // Finally, only include first instance of an interface, so: ClassKey key = new ClassKey(currentType.getErasedType()); - if (seenTypes.contains(key)) { + if (!seenTypes.add(key)) { return; } // If all good so far, append - seenTypes.add(key); types.add(currentType); /* and check supertypes; starting with interfaces. Why interfaces? * So that "highest" interfaces get priority; otherwise we'd recurse diff --git a/src/main/java/com/fasterxml/classmate/TypeResolver.java b/src/main/java/com/fasterxml/classmate/TypeResolver.java index a143036..b37ecab 100644 --- a/src/main/java/com/fasterxml/classmate/TypeResolver.java +++ b/src/main/java/com/fasterxml/classmate/TypeResolver.java @@ -13,7 +13,7 @@ /** * Object that is used for resolving generic type information of a class * so that it is accessible using simple API. Resolved types are also starting - * point for accessing resolved (generics aware) return and argument types + * point for accessing resolved (generics-aware) return and argument types * of class members (methods, fields, constructors). *

* Note that resolver instances are stateful in that resolvers cache resolved @@ -422,10 +422,28 @@ private ResolvedType _constructType(ClassStack context, Class rawType, TypeBi ResolvedType elementType = _fromAny(context, rawType.getComponentType(), typeBindings); return new ResolvedArrayType(rawType, typeBindings, elementType); } - // Work-around/fix for [#33]: if the type has no type parameters, don't include - // typeBindings in the ResolvedType - if (!typeBindings.isEmpty() && rawType.getTypeParameters().length == 0) { - typeBindings = TypeBindings.emptyBindings(); + final TypeVariable[] rawTypeParameters = rawType.getTypeParameters(); + // [classmate#53]: Handle raw generic types - resolve type parameters to their bounds + if (typeBindings.isEmpty()) { + if (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); + } + } else { + // Work-around/fix for [classmate#33]: if the type has no type parameters, + // don't include typeBindings in the ResolvedType + if (rawTypeParameters.length == 0) { + typeBindings = TypeBindings.emptyBindings(); + } } // For other types super interfaces are needed... if (rawType.isInterface()) { diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedArrayType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedArrayType.java index 4630f3a..b7c81d8 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedArrayType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedArrayType.java @@ -7,8 +7,21 @@ public final class ResolvedArrayType extends ResolvedType { - protected final ResolvedType _elementType; + /** + * All Java Arrays extend {@link java.lang.Object} so we need + * a reference + *

+ * Note that direct construction is used instead of construction via + * {@link TypeResolver} due to complexity of doing latter: {@code java.lang.Object} + * also does not implement any interfaces so this should be safe enough. + * + * @since 1.7 + */ + private final static ResolvedObjectType PARENT_TYPE = + ResolvedObjectType.create(Object.class, null, null, null); + protected final ResolvedType _elementType; + /* /********************************************************************** /* Life cycle @@ -34,7 +47,7 @@ public boolean canCreateSubtypes() { */ @Override - public ResolvedType getParentClass() { return null; } + public ResolvedType getParentClass() { return PARENT_TYPE; } @Override public ResolvedType getSelfReferencedType() { return null; } @@ -69,7 +82,7 @@ public boolean isInterface() { /* /********************************************************************** - /* Accessors for raw (minimally procesed) members + /* Accessors for raw (minimally processed) members /********************************************************************** */ diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedObjectType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedObjectType.java index 5b54221..9b354a8 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedObjectType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedObjectType.java @@ -83,20 +83,6 @@ public ResolvedObjectType(Class erased, TypeBindings bindings, _modifiers = erased.getModifiers(); } - @Deprecated // since 1.1; removed from 1.2 -- kept for binary backwards compatibility - public ResolvedObjectType(Class erased, TypeBindings bindings, - ResolvedObjectType superClass, List interfaces) - { - this(erased, bindings, (ResolvedType) superClass, interfaces); - } - - @Deprecated // since 1.1; removed from 1.2 -- kept for binary backwards compatibility - public ResolvedObjectType(Class erased, TypeBindings bindings, - ResolvedObjectType superClass, ResolvedType[] interfaces) - { - this(erased, bindings, (ResolvedType) superClass, interfaces); - } - public static ResolvedObjectType create(Class erased, TypeBindings bindings, ResolvedType superClass, List interfaces) { diff --git a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java index f43143e..efa9e7d 100644 --- a/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java +++ b/src/main/java/com/fasterxml/classmate/types/ResolvedRecursiveType.java @@ -151,23 +151,21 @@ public StringBuilder appendFullDescription(StringBuilder sb) /* Other overrides /********************************************************************** */ - - @Override public boolean equals(Object o) + + // 02-Jan-2026: [classmate#117]: Do NOT compare _referencedType to avoid infinite + // recursion: super.equals() already compares class type, erased type, and type + // bindings, which is sufficient for determining equality of recursive types. + // Comparing _referencedType causes StackOverflowError when comparing types + // from different TypeResolver instances. + /* + @Override + public boolean equals(Object o) { - if (!super.equals(o)) { - return false; - } - // not sure if we should match at all, but definitely need this - // additional part if we do: - ResolvedRecursiveType other = (ResolvedRecursiveType) o; - if (_referencedType == null) { - return other._referencedType == null; - } - return _referencedType.equals(other._referencedType); + return super.equals(o); } + */ // Only for compliance purposes: lgtm.com complains if only equals overridden - @Override public int hashCode() { - return super.hashCode(); - } + // 02-Jan-2026, tatu: No longer, base impl is fine + // @Override public int hashCode() { return super.hashCode(); } } diff --git a/src/test/java/com/fasterxml/classmate/AnnotationsTest.java b/src/test/java/com/fasterxml/classmate/AnnotationsTest.java index 096bed3..6b0a49e 100644 --- a/src/test/java/com/fasterxml/classmate/AnnotationsTest.java +++ b/src/test/java/com/fasterxml/classmate/AnnotationsTest.java @@ -7,17 +7,15 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; -import static junit.framework.Assert.*; -import static org.junit.Assert.assertEquals; - -@SuppressWarnings("deprecation") -public class AnnotationsTest { +import static org.junit.Assert.*; +public class AnnotationsTest +{ @Retention(RetentionPolicy.RUNTIME) private static @interface Marker { } @Test @Marker - public void addAsDefault() throws NoSuchMethodException { + public void addAsDefault() throws Exception { Annotations annotations = new Annotations(); Method thisMethod = AnnotationsTest.class.getDeclaredMethod("addAsDefault"); @@ -39,7 +37,7 @@ public void addAsDefault() throws NoSuchMethodException { } @Test - public void size() throws NoSuchMethodException { + public void size() throws Exception { Annotations annotations = new Annotations(); Method thisMethod = AnnotationsTest.class.getDeclaredMethod("addAsDefault"); @@ -57,7 +55,7 @@ public void size() throws NoSuchMethodException { } @Test - public void annotationsToSize() throws NoSuchMethodException { + public void annotationsToSize() throws Exception { Annotations annotations = new Annotations(); Method thisMethod = AnnotationsTest.class.getDeclaredMethod("addAsDefault"); @@ -67,23 +65,42 @@ public void annotationsToSize() throws NoSuchMethodException { annotations.addAsDefault(testAnnotation); // order is unspecified as the internal representation is a HashMap; just assert the constituent parts are present - String asString = annotations.toString(); + String asString = _normalize(annotations.toString()); assertTrue(asString.contains("{interface org.junit.Test=@org.junit.Test(")); assertTrue(asString.contains("timeout=0")); // 15-Nov-2016, tatu: Java 9 changes description slightly, need to modify - assertTrue(asString.contains("expected=class org.junit.Test$None") // until Java 8 - || asString.contains("expected=org.junit.Test$None")); + // 05-Dec-2025, tatu: Java 21 adds further variation + if (!(asString.contains("expected=class org.junit.Test.None") // until Java 8 + || asString.contains("expected=org.junit.Test.None"))) { + fail("No 'expected' in: "+asString); + } Annotation markerAnnotation = thisMethod.getAnnotation(Marker.class); annotations.addAsDefault(markerAnnotation); - asString = annotations.toString(); - assertTrue(asString.contains("interface com.fasterxml.classmate.AnnotationsTest$Marker=@com.fasterxml.classmate.AnnotationsTest$Marker()")); + asString = _normalize(annotations.toString()); + + String exp = "interface com.fasterxml.classmate.AnnotationsTest.Marker=@com.fasterxml.classmate.AnnotationsTest.Marker()"; + if (!asString.contains(exp)) { + fail("Expected: ["+exp+"]\nin ["+asString+"]"); + } assertTrue(asString.contains("interface org.junit.Test=@org.junit.Test")); assertTrue(asString.contains("timeout=0")); // 15-Nov-2016, tatu: Java 9 changes description slightly, need to modify - assertTrue(asString.contains("expected=class org.junit.Test$None") - || asString.contains("expected=org.junit.Test$None")); + // 05-Dec-2025, tatu: Java 21 adds further variation + if (!(asString.contains("expected=class org.junit.Test.None") // until Java 8 + || asString.contains("expected=org.junit.Test.None"))) { + fail("No 'expected' in: "+asString); + } + } + + private static String _normalize(String str) { + // 05-Dec-2025, tatu: Java 21 changes from "org.junit.Test$None" to "org.junit.Test.None" + String str2; + while ((str2 = str.replace('$', '.')) != str) { + str = str2; + } + return str; } } diff --git a/src/test/java/com/fasterxml/classmate/TestMemberResolver.java b/src/test/java/com/fasterxml/classmate/MemberResolverTest.java similarity index 92% rename from src/test/java/com/fasterxml/classmate/TestMemberResolver.java rename to src/test/java/com/fasterxml/classmate/MemberResolverTest.java index 38965b1..00db56c 100644 --- a/src/test/java/com/fasterxml/classmate/TestMemberResolver.java +++ b/src/test/java/com/fasterxml/classmate/MemberResolverTest.java @@ -1,6 +1,5 @@ package com.fasterxml.classmate; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; @@ -8,7 +7,7 @@ import com.fasterxml.classmate.types.ResolvedObjectType; import com.fasterxml.classmate.util.ClassKey; -public class TestMemberResolver extends BaseTest +public class MemberResolverTest extends BaseTest { /* /********************************************************************** @@ -214,7 +213,11 @@ public void testIncludeObject() mr.setIncludeLangObject(true); simpleResolvedTypeWithMembers = mr.resolve(simpleResolvedType, null, null); - assertEquals(12, simpleResolvedTypeWithMembers.getMemberMethods().length); + // With JDK < 21, 12 member methods, 21+ 13 + int methodCount = simpleResolvedTypeWithMembers.getMemberMethods().length; + if (methodCount < 12 || methodCount > 13) { + fail("Expected [12, 13] methods, got: "+methodCount); + } assertEquals(1, simpleResolvedTypeWithMembers.getMemberFields().length); } @@ -225,7 +228,12 @@ public void testFilters() mr.setIncludeLangObject(true); ResolvedTypeWithMembers simpleResolvedTypeWithMembers = mr.resolve(simpleResolvedType, null, null); - assertEquals(12, simpleResolvedTypeWithMembers.getMemberMethods().length); + // With JDK < 21, 12 member methods, 21+ 13 + int methodCount = simpleResolvedTypeWithMembers.getMemberMethods().length; + if (methodCount < 12 || methodCount > 13) { + fail("Expected [12, 13] methods, got: "+methodCount); + } + assertEquals(1, simpleResolvedTypeWithMembers.getMemberFields().length); assertEquals(2, simpleResolvedTypeWithMembers.getConstructors().length); @@ -253,7 +261,12 @@ public void testFilters() }); simpleResolvedTypeWithMembers = mr.resolve(simpleResolvedType, null, null); - assertEquals(12, simpleResolvedTypeWithMembers.getMemberMethods().length); + + // With JDK < 21, 12 member methods, 21+ 13 + methodCount = simpleResolvedTypeWithMembers.getMemberMethods().length; + if (methodCount < 12 || methodCount > 13) { + fail("Expected [12, 13] methods, got: "+methodCount); + } assertEquals(0, simpleResolvedTypeWithMembers.getMemberFields().length); assertEquals(2, simpleResolvedTypeWithMembers.getConstructors().length); @@ -267,11 +280,19 @@ public void testFilters() }); simpleResolvedTypeWithMembers = mr.resolve(simpleResolvedType, null, null); - assertEquals(12, simpleResolvedTypeWithMembers.getMemberMethods().length); + // With JDK < 21, 12 member methods, 21+ 13 + methodCount = simpleResolvedTypeWithMembers.getMemberMethods().length; + if (methodCount < 12 || methodCount > 13) { + fail("Expected [12, 13] methods, got: "+methodCount); + } assertEquals(1, simpleResolvedTypeWithMembers.getMemberFields().length); assertEquals(1, simpleResolvedTypeWithMembers.getConstructors().length); } + // 10-Oct-2023, tatu: Why, what, how? I don't think internals should be tested + // like this. And since this breaks with JDK 17 (probably due to additional + // interfaces in JDK types), will comment out. +/* public void testAddOverridesFromInterfaces() throws IllegalAccessException, InvocationTargetException { ResolvedType resolvedType = typeResolver.resolve(MemberResolver.class); @@ -317,6 +338,7 @@ public void testAddOverridesFromInterfaces() throws IllegalAccessException, Invo assertEquals(2, seenTypes.size()); assertEquals(2, typesWithOverrides.size()); } + */ public void testGatherTypesWithInterfaces() throws Exception { diff --git a/src/test/java/com/fasterxml/classmate/ResolvedTypeTest.java b/src/test/java/com/fasterxml/classmate/ResolvedTypeTest.java index 07666bc..ae60492 100644 --- a/src/test/java/com/fasterxml/classmate/ResolvedTypeTest.java +++ b/src/test/java/com/fasterxml/classmate/ResolvedTypeTest.java @@ -12,7 +12,7 @@ public class ResolvedTypeTest extends BaseTest { - // For [Issue#16] + // For [classmate#16] static class Foo16 extends Bar16 { } @@ -35,11 +35,10 @@ public void testCanCreateSubtype() { assertFalse(arrayType.canCreateSubtype(String[].class)); assertFalse(arrayType.canCreateSubtype(CharBuffer[].class)); assertFalse(arrayType.canCreateSubtype(String.class)); - } @Test - public void testtypeParametersFor() { + public void testTypeParametersFor() { ResolvedObjectType stringType = ResolvedObjectType.create(String.class, null, null, null); assertNull(stringType.typeParametersFor(CharBuffer.class)); } @@ -113,7 +112,7 @@ public void testAccessors() { assertEquals(0, type.getStaticFields().size()); } - // For [Issue#16] + // For [classmate#16] @Test public void testIssue16() { diff --git a/src/test/java/com/fasterxml/classmate/ResolvedTypeWithMembersTest.java b/src/test/java/com/fasterxml/classmate/ResolvedTypeWithMembersTest.java index 8fb5b96..dfeffde 100644 --- a/src/test/java/com/fasterxml/classmate/ResolvedTypeWithMembersTest.java +++ b/src/test/java/com/fasterxml/classmate/ResolvedTypeWithMembersTest.java @@ -16,9 +16,8 @@ /** * User: blangel */ -@SuppressWarnings("deprecation") -public class ResolvedTypeWithMembersTest { - +public class ResolvedTypeWithMembersTest +{ @Retention(RetentionPolicy.RUNTIME) static @interface Marker { } diff --git a/src/test/java/com/fasterxml/classmate/StdConfigurationTest.java b/src/test/java/com/fasterxml/classmate/StdConfigurationTest.java index 62323cd..bb6a08c 100644 --- a/src/test/java/com/fasterxml/classmate/StdConfigurationTest.java +++ b/src/test/java/com/fasterxml/classmate/StdConfigurationTest.java @@ -5,9 +5,8 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertSame; -@SuppressWarnings("deprecation") -public class StdConfigurationTest { - +public class StdConfigurationTest +{ @Test public void getInclusionForClass() { AnnotationConfiguration.StdConfiguration config = new AnnotationConfiguration.StdConfiguration(null); diff --git a/src/test/java/com/fasterxml/classmate/TestReadme.java b/src/test/java/com/fasterxml/classmate/TestReadme.java index 01a4df5..55d113d 100644 --- a/src/test/java/com/fasterxml/classmate/TestReadme.java +++ b/src/test/java/com/fasterxml/classmate/TestReadme.java @@ -39,7 +39,6 @@ public static class SomeOtherClass { public void someMethod() { } } - @Test @SuppressWarnings("serial") public void resolvingClasses() { TypeResolver typeResolver = new TypeResolver(); ResolvedType listType = typeResolver.resolve(List.class); diff --git a/src/test/java/com/fasterxml/classmate/TestTypeDescriptions.java b/src/test/java/com/fasterxml/classmate/TestTypeDescriptions.java index 567e107..ae224ef 100644 --- a/src/test/java/com/fasterxml/classmate/TestTypeDescriptions.java +++ b/src/test/java/com/fasterxml/classmate/TestTypeDescriptions.java @@ -32,12 +32,24 @@ public void testSimpleTypes() assertEquals("Ljava/lang/Object;", objectType.getSignature()); ResolvedType stringType = typeResolver.resolve(String.class); - /* Interesting thing with "simple" type like java.lang.String is that - * it has recursive type self-reference (via Comparable) - */ - assertEquals("java.lang.String extends java.lang.Object" - +" implements java.io.Serializable,java.lang.Comparable,java.lang.CharSequence", - stringType.getFullDescription()); + // Interesting thing with "simple" type like java.lang.String is that + // it has recursive type self-reference (via Comparable) + + final String stringDesc = stringType.getFullDescription(); + + // 10-Oct-2023, tatu: With JDK 17, get even more stuff... Start with pre-17 desc: + if (stringDesc.equals("java.lang.String extends java.lang.Object" + +" implements java.io.Serializable,java.lang.Comparable,java.lang.CharSequence" + )) { + ; + // But then allow JDK 17 variant + } else if (stringDesc.equals("java.lang.String extends java.lang.Object" + +" implements java.io.Serializable,java.lang.Comparable,java.lang.CharSequence" + +",java.lang.constant.Constable,java.lang.constant.ConstantDesc" + )) { + } else { + fail("Full String description not matching one of expected signatures: "+stringDesc); + } assertEquals("Ljava/lang/String;", stringType.getErasedSignature()); assertEquals("Ljava/lang/String;", stringType.getSignature()); } @@ -56,7 +68,6 @@ public void testPrimitiveTypes() assertEquals("boolean", boolType.getFullDescription()); } - @SuppressWarnings("serial") public void testGenericTypes() { ResolvedType mapType = typeResolver.resolve(new GenericType>() { }); diff --git a/src/test/java/com/fasterxml/classmate/TypeBindingsTest.java b/src/test/java/com/fasterxml/classmate/TypeBindingsTest.java index 9da1422..537f267 100644 --- a/src/test/java/com/fasterxml/classmate/TypeBindingsTest.java +++ b/src/test/java/com/fasterxml/classmate/TypeBindingsTest.java @@ -9,7 +9,6 @@ import static junit.framework.Assert.*; -@SuppressWarnings("deprecation") public class TypeBindingsTest { @Test diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver117Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver117Test.java new file mode 100644 index 0000000..717010f --- /dev/null +++ b/src/test/java/com/fasterxml/classmate/TypeResolver117Test.java @@ -0,0 +1,613 @@ +package com.fasterxml.classmate; + +import java.util.List; + +import com.fasterxml.classmate.members.RawMethod; +import com.fasterxml.classmate.members.ResolvedMethod; + +/** + * Test for [classmate#117]: StackOverflowError in 1.7.2 with recursive types + * + * NOTE: This test attempts to reproduce a StackOverflowError that occurs due to + * infinite recursion in equals() methods when comparing recursive types: + * - TypeBindings.equals() (line 221) -> ResolvedType.equals() + * - ResolvedType.equals() (line 281) -> TypeBindings.equals() + * - ResolvedRecursiveType.equals() (lines 157, 166) -> super.equals() + _referencedType.equals() + * + * The issue was introduced in commit 57fb93a which added support for resolving + * raw generic types by binding type parameters to their bounds. This can create + * circular dependencies in ResolvedRecursiveType instances. + */ +public class TypeResolver117Test extends BaseTest +{ + protected final TypeResolver RESOLVER = new TypeResolver(); + + // Test with a custom recursive type similar to Enum> + static interface SelfReferential> { + T self(); + } + + @SuppressWarnings("rawtypes") + static abstract class RawSelfReferential implements SelfReferential { + } + + // Another recursive pattern with class + static abstract class SelfBounded> { + } + + @SuppressWarnings("rawtypes") + static abstract class RawSelfBounded extends SelfBounded { + } + + // Real enum to test with + static enum TestEnum { + A, B, C + } + + // Class that uses raw Enum in its hierarchy + @SuppressWarnings("rawtypes") + static abstract class UsesRawComparable implements Comparable { + } + + // More complex recursive patterns that might trigger the issue + + // Double-nested recursive type + static abstract class DoublyRecursive, U extends DoublyRecursive> { + } + + @SuppressWarnings("rawtypes") + static abstract class RawDoublyRecursive extends DoublyRecursive { + } + + // Recursive type that implements another recursive type + static abstract class RecursiveChain> + extends SelfBounded> { + } + + @SuppressWarnings("rawtypes") + static abstract class RawRecursiveChain extends RecursiveChain { + } + + /** + * This test reproduces the StackOverflowError reported in issue #117. + * When resolving a raw self-bounded type, the equals() comparison of recursive + * types causes infinite recursion: + * - ResolvedType.equals() calls TypeBindings.equals() + * - TypeBindings.equals() calls ResolvedType.equals() on contained types + * - For ResolvedRecursiveType, this calls both super.equals() AND + * _referencedType.equals(), creating a cycle + */ + public void testRawSelfBoundedCausesStackOverflow() { + // This should not throw StackOverflowError + ResolvedType rt = RESOLVER.resolve(RawSelfBounded.class); + assertNotNull(rt); + // If we get here without StackOverflowError, the test passes + } + + public void testRawSelfReferentialInterface() { + // Test with raw interface that has self-referential type parameter + ResolvedType rt = RESOLVER.resolve(RawSelfReferential.class); + assertNotNull(rt); + } + + public void testRealEnumType() { + // Test with a real enum type (Enum>) + ResolvedType rt = RESOLVER.resolve(TestEnum.class); + assertNotNull(rt); + } + + public void testRawComparableViaEnum() { + // Test with class implementing raw Comparable + // (Comparable is the interface that Enum implements) + ResolvedType rt = RESOLVER.resolve(UsesRawComparable.class); + assertNotNull(rt); + } + + /** + * This test checks that equals() on recursive types doesn't cause + * infinite recursion + */ + public void testRecursiveTypeEquals() { + ResolvedType rt1 = RESOLVER.resolve(RawSelfBounded.class); + ResolvedType rt2 = RESOLVER.resolve(RawSelfBounded.class); + + // This equals comparison should not cause StackOverflowError + assertEquals(rt1, rt2); + } + + /** + * Test equals with self-referential interface + */ + public void testRecursiveInterfaceEquals() { + ResolvedType rt1 = RESOLVER.resolve(RawSelfReferential.class); + ResolvedType rt2 = RESOLVER.resolve(RawSelfReferential.class); + + // This equals comparison should not cause StackOverflowError + assertEquals(rt1, rt2); + } + + /** + * Test with doubly-recursive type (two type parameters that reference each other) + */ + public void testDoublyRecursiveType() { + ResolvedType rt = RESOLVER.resolve(RawDoublyRecursive.class); + assertNotNull(rt); + } + + /** + * Test with recursive chain (recursive type extending another recursive type) + */ + public void testRecursiveChain() { + ResolvedType rt = RESOLVER.resolve(RawRecursiveChain.class); + assertNotNull(rt); + } + + /** + * Test equals on doubly-recursive type + */ + public void testDoublyRecursiveEquals() { + ResolvedType rt1 = RESOLVER.resolve(RawDoublyRecursive.class); + ResolvedType rt2 = RESOLVER.resolve(RawDoublyRecursive.class); + + // This equals comparison should not cause StackOverflowError + assertEquals(rt1, rt2); + } + + /** + * Direct test with java.lang.Enum + * This is the most likely candidate for reproducing the issue + * since Enum has the recursive pattern: Enum> + */ + public void testDirectEnumResolution() { + // Try resolving Enum.class directly (as a raw type) + @SuppressWarnings("rawtypes") + Class enumClass = Enum.class; + ResolvedType rt = RESOLVER.resolve(enumClass); + assertNotNull(rt); + } + + /** + * Test equals on Enum type + */ + public void testEnumTypeEquals() { + @SuppressWarnings("rawtypes") + Class enumClass = Enum.class; + ResolvedType rt1 = RESOLVER.resolve(enumClass); + ResolvedType rt2 = RESOLVER.resolve(enumClass); + + // This equals comparison should not cause StackOverflowError + assertEquals(rt1, rt2); + } + + /** + * Stress test: resolve many recursive types to trigger caching/equality checks + */ + public void testMultipleRecursiveResolutions() { + // Resolve the same types multiple times + for (int i = 0; i < 10; i++) { + ResolvedType rt1 = RESOLVER.resolve(RawSelfBounded.class); + ResolvedType rt2 = RESOLVER.resolve(RawSelfReferential.class); + ResolvedType rt3 = RESOLVER.resolve(RawDoublyRecursive.class); + + // Force equals checks + assertEquals(rt1, RESOLVER.resolve(RawSelfBounded.class)); + assertEquals(rt2, RESOLVER.resolve(RawSelfReferential.class)); + assertEquals(rt3, RESOLVER.resolve(RawDoublyRecursive.class)); + } + } + + /** + * Test resolving type parameters directly + */ + public void testRecursiveTypeParameters() { + @SuppressWarnings("rawtypes") + Class enumClass = Enum.class; + ResolvedType rt = RESOLVER.resolve(enumClass); + + // Get the type parameters which should include the recursive bound + List typeParams = rt.getTypeParameters(); + assertNotNull(typeParams); + + // For raw Enum, the type parameter E should be resolved to its bound: Enum + // This creates a ResolvedRecursiveType which could trigger the equals issue + if (!typeParams.isEmpty()) { + ResolvedType param = typeParams.get(0); + assertNotNull(param); + + // Try to compare it + assertEquals(param, param); + + // Check if it's a recursive type + ResolvedType selfRef = param.getSelfReferencedType(); + if (selfRef != null) { + // This is a ResolvedRecursiveType - try comparing it + assertEquals(param, param); + } + } + } + + /** + * Deep inspection test: examine the structure of resolved recursive types + * to understand if the cycle exists + */ + public void testRecursiveTypeStructure() { + ResolvedType rt = RESOLVER.resolve(RawSelfBounded.class); + + // Get parent class which should be SelfBounded with type parameters + ResolvedType parent = rt.getParentClass(); + if (parent != null) { + List parentParams = parent.getTypeParameters(); + if (!parentParams.isEmpty()) { + ResolvedType param = parentParams.get(0); + + // If this is recursive, it might have a self-reference + ResolvedType selfRef = param.getSelfReferencedType(); + if (selfRef != null) { + // Try to trigger the equals issue + TypeBindings bindings1 = parent.getTypeBindings(); + TypeBindings bindings2 = parent.getTypeBindings(); + + // This should not cause StackOverflowError + assertEquals(bindings1, bindings2); + } + } + } + } + + /** + * Test that explicitly verifies the issue is resolved or documents + * that it could not be reproduced in unit tests + */ + public void testIssue117Summary() { + // This test documents the investigation of issue #117 + + // Try all the patterns that should trigger the issue: + // 1. Raw self-bounded type + ResolvedType rt1 = RESOLVER.resolve(RawSelfBounded.class); + assertEquals(rt1, rt1); + + // 2. Raw Enum (the classic case) + ResolvedType rt2 = RESOLVER.resolve(Enum.class); + assertEquals(rt2, rt2); + + // 3. Doubly recursive type + ResolvedType rt3 = RESOLVER.resolve(RawDoublyRecursive.class); + assertEquals(rt3, rt3); + + // If we reach here without StackOverflowError, either: + // a) The issue has been fixed in the current code + // b) The issue requires a specific integration scenario not covered by unit tests + // c) The issue manifests only with certain JDK versions or configurations + + assertTrue("Issue #117 tests completed without StackOverflowError", true); + } + + /** + * Test using MemberResolver which might trigger more complex resolution + * and cache lookups + */ + public void testMemberResolverWithRecursiveTypes() { + MemberResolver memberResolver = new MemberResolver(RESOLVER); + + // Resolve a type that has recursive bounds + ResolvedType rt = RESOLVER.resolve(RawSelfBounded.class); + + // Get members which forces resolution of method return types, parameter types, etc. + ResolvedTypeWithMembers members = memberResolver.resolve(rt, null, null); + assertNotNull(members); + + // Access methods to trigger lazy resolution + ResolvedMethod[] methods = members.getMemberMethods(); + assertNotNull(methods); + } + + /** + * Test MemberResolver with java.lang.Enum + */ + public void testMemberResolverWithEnum() { + MemberResolver memberResolver = new MemberResolver(RESOLVER); + + // Resolve raw Enum + ResolvedType rt = RESOLVER.resolve(Enum.class); + + // Get members - this might trigger the stack overflow + ResolvedTypeWithMembers members = memberResolver.resolve(rt, null, null); + assertNotNull(members); + + ResolvedMethod[] methods = members.getMemberMethods(); + assertNotNull(methods); + } + + /** + * Test with multiple TypeResolver instances to ensure no cross-cache issues + */ + public void testMultipleResolvers() { + TypeResolver resolver1 = new TypeResolver(); + TypeResolver resolver2 = new TypeResolver(); + + ResolvedType rt1 = resolver1.resolve(RawSelfBounded.class); + ResolvedType rt2 = resolver2.resolve(RawSelfBounded.class); + + // These should be equal even from different resolvers + assertEquals(rt1, rt2); + } + + /** + * Test that specifically tries to trigger cache comparison by + * resolving the same type multiple times + */ + public void testCacheLookupWithRecursiveTypes() { + // First resolution - should cache it + ResolvedType rt1 = RESOLVER.resolve(Enum.class); + + // Second resolution - should find in cache and compare keys + // This is where the StackOverflowError might occur due to + // ResolvedTypeKey.equals() calling ResolvedType.equals() + ResolvedType rt2 = RESOLVER.resolve(Enum.class); + + // Should be the same instance from cache + assertSame(rt1, rt2); + + // Also test equals explicitly + assertEquals(rt1, rt2); + } + + /** + * Test resolving type parameters for recursive types + */ + public void testTypeParametersForRecursiveTypes() { + // Resolve Enum + ResolvedType enumType = RESOLVER.resolve(Enum.class); + + // Get its type parameters + List typeParams = enumType.getTypeParameters(); + assertEquals(1, typeParams.size()); + + ResolvedType paramType = typeParams.get(0); + + // The parameter should be a ResolvedRecursiveType for Enum + // Try to get its type parameters recursively + List nestedParams = paramType.getTypeParameters(); + + // Now try to compare - this might trigger the stack overflow + assertEquals(paramType, paramType); + + if (!nestedParams.isEmpty()) { + ResolvedType nestedParam = nestedParams.get(0); + // This comparison might cause issues + assertEquals(nestedParam, nestedParam); + } + } + + /** + * Test with typeParametersFor which might trigger different code paths + */ + public void testTypeParametersForMethod() { + ResolvedType rt = RESOLVER.resolve(TestEnum.class); + + // TestEnum extends Enum, so asking for Enum's type parameters + // should return TestEnum + List params = rt.typeParametersFor(Enum.class); + assertNotNull(params); + assertEquals(1, params.size()); + + // The parameter should be TestEnum + ResolvedType param = params.get(0); + assertEquals(TestEnum.class, param.getErasedType()); + } + + /** + * Test comparing TypeBindings directly + */ + public void testTypeBindingsComparison() { + ResolvedType rt1 = RESOLVER.resolve(Enum.class); + ResolvedType rt2 = RESOLVER.resolve(Enum.class); + + TypeBindings bindings1 = rt1.getTypeBindings(); + TypeBindings bindings2 = rt2.getTypeBindings(); + + // This should trigger TypeBindings.equals() which calls + // ResolvedType.equals() on contained types + assertEquals(bindings1, bindings2); + } + + /** + * *** THIS TEST REPRODUCES THE STACKOVERFLOWERROR *** + * + * Test with FRESH TypeResolver instances to avoid cache hits + * This forces new resolution and creation of ResolvedRecursiveTypes. + * When comparing ResolvedTypes from different resolvers, the equals() + * method triggers infinite recursion through: + * - ResolvedRecursiveType.equals() -> super.equals() -> TypeBindings.equals() + * - TypeBindings.equals() -> ResolvedType.equals() (on type parameters) + * - ResolvedRecursiveType.equals() -> _referencedType.equals() -> ... (cycle) + */ + public void testFreshResolversWithRecursiveTypes() { + // Use fresh resolver each time to avoid caching + TypeResolver fresh1 = new TypeResolver(); + TypeResolver fresh2 = new TypeResolver(); + + ResolvedType rt1 = fresh1.resolve(Enum.class); + ResolvedType rt2 = fresh2.resolve(Enum.class); + + // These are from different resolvers, so different instances + // Comparing them should trigger full equals() logic + assertEquals(rt1, rt2); + + // Also compare their type bindings + assertEquals(rt1.getTypeBindings(), rt2.getTypeBindings()); + + // And compare type parameters + List params1 = rt1.getTypeParameters(); + List params2 = rt2.getTypeParameters(); + + if (!params1.isEmpty() && !params2.isEmpty()) { + // This comparison between ResolvedRecursiveTypes from different + // resolvers might trigger the infinite recursion + assertEquals(params1.get(0), params2.get(0)); + } + } + + /** + * Test resolving through different paths to create different + * but equivalent recursive type structures + */ + public void testDifferentResolutionPaths() { + TypeResolver resolver = new TypeResolver(); + + // Path 1: Resolve Enum directly + ResolvedType enumDirect = resolver.resolve(Enum.class); + + // Path 2: Resolve through an enum subclass and ask for parent + ResolvedType testEnumType = resolver.resolve(TestEnum.class); + ResolvedType enumViaParent = testEnumType.getParentClass(); + + // These might have different ResolvedRecursiveType instances internally + // Comparing them could trigger the issue + if (enumViaParent != null) { + // Compare the types + assertNotNull(enumDirect); + assertNotNull(enumViaParent); + + // The erased types should be the same + assertEquals(Enum.class, enumViaParent.getErasedType()); + } + } + + /** + * *** THIS TEST ALSO REPRODUCES THE STACKOVERFLOWERROR *** + * + * Test that creates maximum pressure on equals() by resolving + * many recursive types and comparing them all. + * This amplifies the issue by comparing types from multiple different resolvers. + */ + public void testMassiveRecursiveTypeComparison() { + TypeResolver[] resolvers = new TypeResolver[5]; + ResolvedType[][] types = new ResolvedType[5][4]; + + // Create multiple resolvers and resolve multiple recursive types in each + for (int i = 0; i < 5; i++) { + resolvers[i] = new TypeResolver(); + types[i][0] = resolvers[i].resolve(Enum.class); + types[i][1] = resolvers[i].resolve(RawSelfBounded.class); + types[i][2] = resolvers[i].resolve(RawSelfReferential.class); + types[i][3] = resolvers[i].resolve(RawDoublyRecursive.class); + } + + // Now compare all of them - this creates many equals() calls + for (int typeIdx = 0; typeIdx < 4; typeIdx++) { + for (int i = 0; i < 5; i++) { + for (int j = i + 1; j < 5; j++) { + // Compare types from different resolvers + assertEquals(types[i][typeIdx], types[j][typeIdx]); + } + } + } + } + + /** + * Test findSupertype which might trigger different resolution paths + */ + public void testFindSupertypeWithRecursive() { + ResolvedType testEnumType = RESOLVER.resolve(TestEnum.class); + + // Find Enum supertype - this might trigger different code paths + ResolvedType enumSupertype = testEnumType.findSupertype(Enum.class); + assertNotNull(enumSupertype); + + // Find Comparable supertype (implemented by Enum) + ResolvedType comparableSupertype = testEnumType.findSupertype(Comparable.class); + assertNotNull(comparableSupertype); + + // Compare them + assertNotSame(enumSupertype, comparableSupertype); + } + + /** + * Test that verifies caching still works correctly with the fix. + * This tests that cache lookups succeed when comparing ResolvedTypeKeys + * that contain ResolvedRecursiveType instances. + */ + public void testCacheCorrectness() { + TypeResolver resolver = new TypeResolver(); + + // Resolve Enum first time - should be cached + ResolvedType enum1 = resolver.resolve(Enum.class); + assertNotNull(enum1); + + // Resolve Enum second time - should get from cache (same instance) + ResolvedType enum2 = resolver.resolve(Enum.class); + assertSame("Should return same instance from cache", enum1, enum2); + + // Verify type parameters contain recursive types + List params = enum1.getTypeParameters(); + assertEquals(1, params.size()); + + ResolvedType paramType = params.get(0); + // Check if it's a recursive type + ResolvedType selfRef = paramType.getSelfReferencedType(); + if (selfRef != null) { + // It's a ResolvedRecursiveType - verify it works correctly + assertNotNull(selfRef); + } + + // Resolve RawSelfBounded - also has recursive structure + ResolvedType sb1 = resolver.resolve(RawSelfBounded.class); + ResolvedType sb2 = resolver.resolve(RawSelfBounded.class); + assertSame("RawSelfBounded should also be cached", sb1, sb2); + } + + /** + * Test that ResolvedRecursiveType instances with same structure + * but from different resolvers are equal (but not same instance) + */ + public void testRecursiveTypeEqualityAcrossResolvers() { + TypeResolver resolver1 = new TypeResolver(); + TypeResolver resolver2 = new TypeResolver(); + + ResolvedType enum1 = resolver1.resolve(Enum.class); + ResolvedType enum2 = resolver2.resolve(Enum.class); + + // Should be equal (structural equality) + assertEquals("Types from different resolvers should be equal", enum1, enum2); + + // But not same instance + assertNotSame("Should not be same instance", enum1, enum2); + + // Type parameters should also be equal + List params1 = enum1.getTypeParameters(); + List params2 = enum2.getTypeParameters(); + + assertEquals(params1.size(), params2.size()); + for (int i = 0; i < params1.size(); i++) { + // This is the critical test - comparing ResolvedRecursiveTypes + // from different resolvers should work without StackOverflow + assertEquals("Type parameter " + i + " should be equal", + params1.get(i), params2.get(i)); + } + } + + /** + * Verify that _referencedType is only used for navigation, + * not for equality determination + */ + public void testReferencedTypeIsForNavigation() { + ResolvedType enumType = RESOLVER.resolve(Enum.class); + List params = enumType.getTypeParameters(); + + if (!params.isEmpty()) { + ResolvedType param = params.get(0); + ResolvedType selfRef = param.getSelfReferencedType(); + + if (selfRef != null) { + // selfRef should be non-null for ResolvedRecursiveType + assertNotNull(selfRef); + + // selfRef should allow navigation to members + // (this is what _referencedType is used for) + List methods = selfRef.getMemberMethods(); + assertNotNull("Should be able to get methods via referenced type", methods); + } + } + } +} diff --git a/src/test/java/com/fasterxml/classmate/TypeResolver53Test.java b/src/test/java/com/fasterxml/classmate/TypeResolver53Test.java new file mode 100644 index 0000000..4c4c1c0 --- /dev/null +++ b/src/test/java/com/fasterxml/classmate/TypeResolver53Test.java @@ -0,0 +1,70 @@ +package com.fasterxml.classmate; + +import java.util.*; + +// for [classmate#53]: Raw Comparator +public class TypeResolver53Test extends BaseTest +{ + @SuppressWarnings("rawtypes") + static abstract class Comparator53 implements Comparator { } + + @SuppressWarnings("rawtypes") + static abstract class Map53 implements Map { } + + @SuppressWarnings("rawtypes") + static abstract class BoundedComparable implements Comparable { } + + @SuppressWarnings("rawtypes") + static abstract class BoundedRaw extends BoundedComparable { } + + @SuppressWarnings({ "rawtypes", "serial" }) + static abstract class NestedRaw extends ArrayList { } + + protected final TypeResolver RESOLVER = new TypeResolver(); + + // [classmate#53] Problem with Raw types - single type parameter + public void testResolvingRawType() { + ResolvedType rt = RESOLVER.resolve(Comparator53.class); + List params = rt.typeParametersFor(Comparator.class); + assertEquals(Arrays.asList(RESOLVER.resolve(Object.class)), + params); + } + + // [classmate#53] Raw type with multiple type parameters (Map) + public void testRawTypeMultipleParameters() { + ResolvedType rt = RESOLVER.resolve(Map53.class); + List params = rt.typeParametersFor(Map.class); + assertEquals(Arrays.asList( + RESOLVER.resolve(Object.class), + RESOLVER.resolve(Object.class)), + params); + } + + // [classmate#53] Raw type with bounded type parameter + public void testRawTypeWithBoundedParameter() { + ResolvedType rt = RESOLVER.resolve(BoundedRaw.class); + List params = rt.typeParametersFor(Comparable.class); + // BoundedComparable implements Comparable, used raw + // Type parameter T has bound Number, so should resolve to Number + assertEquals(Arrays.asList(RESOLVER.resolve(Number.class)), + params); + } + + // [classmate#53] Nested raw types (List where Map is raw) + public void testNestedRawType() { + ResolvedType rt = RESOLVER.resolve(NestedRaw.class); + List params = rt.typeParametersFor(java.util.ArrayList.class); + assertEquals(1, params.size()); + + // The type parameter should be Map (raw), which should have its own parameters + ResolvedType mapType = params.get(0); + assertEquals(Map.class, mapType.getErasedType()); + + // The raw Map should have Object,Object as its type parameters + List mapParams = mapType.getTypeParameters(); + assertEquals(Arrays.asList( + RESOLVER.resolve(Object.class), + RESOLVER.resolve(Object.class)), + mapParams); + } +} diff --git a/src/test/java/com/fasterxml/classmate/TestTypeResolver.java b/src/test/java/com/fasterxml/classmate/TypeResolverTest.java similarity index 94% rename from src/test/java/com/fasterxml/classmate/TestTypeResolver.java rename to src/test/java/com/fasterxml/classmate/TypeResolverTest.java index b2c0405..bf34231 100644 --- a/src/test/java/com/fasterxml/classmate/TestTypeResolver.java +++ b/src/test/java/com/fasterxml/classmate/TypeResolverTest.java @@ -10,7 +10,7 @@ import com.fasterxml.classmate.util.ResolvedTypeKey; @SuppressWarnings("serial") -public class TestTypeResolver extends BaseTest +public class TypeResolverTest extends BaseTest { /* /********************************************************************** @@ -81,14 +81,8 @@ static class HashTree extends HashMap> { } /********************************************************************** */ - protected TypeResolver typeResolver; - - @Override - protected void setUp() - { - // Let's use a single instance for all tests, to increase chance of seeing failures - typeResolver = new TypeResolver(); - } + // Let's use a single instance for all tests, to increase chance of seeing failures + protected final TypeResolver typeResolver = new TypeResolver(); /* /********************************************************************** @@ -504,7 +498,27 @@ public void testTypesMatch() throws IllegalAccessException, InvocationTargetExce assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchAListResolved, matchBListResolved)); assertFalse((Boolean) typesMatchMethod.invoke(typeResolver, matchBListResolved, matchAListResolved)); } - + + public void testMultiDimensionalGenericArrays() throws Exception + { + ResolvedType resolvedType = typeResolver.resolve(new GenericType[][]>() { }); + assertEquals(List[][].class, resolvedType.getErasedType()); + assertEquals(Collections.emptyList(), resolvedType.getTypeParameters()); + assertTrue(resolvedType.isArray()); + ResolvedArrayType arrayType = (ResolvedArrayType) resolvedType; + ResolvedType inner1 = arrayType.getArrayElementType(); + assertEquals(List[].class, inner1.getErasedType()); + assertTrue(inner1.isArray()); + ResolvedArrayType arrayType2 = (ResolvedArrayType) inner1; + ResolvedType inner2 = arrayType2.getArrayElementType(); + assertEquals(List.class, inner2.getErasedType()); + List inner2TypeParams = inner2.getTypeParameters(); + assertEquals(1, inner2TypeParams.size()); + assertEquals(String.class, inner2TypeParams.get(0).getErasedType()); + + assertEquals("java.util.List[][]", resolvedType.toString()); + } + /* /********************************************************************** /* Helper methods diff --git a/src/test/java/com/fasterxml/classmate/members/HierarchicTypeTest.java b/src/test/java/com/fasterxml/classmate/members/HierarchicTypeTest.java index 8d23b96..35ecd4e 100644 --- a/src/test/java/com/fasterxml/classmate/members/HierarchicTypeTest.java +++ b/src/test/java/com/fasterxml/classmate/members/HierarchicTypeTest.java @@ -9,9 +9,8 @@ /** * @author blangel */ -@SuppressWarnings("deprecation") -public class HierarchicTypeTest { - +public class HierarchicTypeTest +{ @Test public void hierarchicTypeToString() { HierarchicType hierarchicType = new HierarchicType(null, false, 0); diff --git a/src/test/java/com/fasterxml/classmate/members/RawFieldTest.java b/src/test/java/com/fasterxml/classmate/members/RawFieldTest.java index f909c32..e7ce576 100644 --- a/src/test/java/com/fasterxml/classmate/members/RawFieldTest.java +++ b/src/test/java/com/fasterxml/classmate/members/RawFieldTest.java @@ -10,9 +10,8 @@ import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; -@SuppressWarnings("deprecation") -public class RawFieldTest { - +public class RawFieldTest +{ @SuppressWarnings("unused") private static class ModifiersClass { private static String test; diff --git a/src/test/java/com/fasterxml/classmate/members/RawMethodTest.java b/src/test/java/com/fasterxml/classmate/members/RawMethodTest.java index a6f1fe5..2759460 100644 --- a/src/test/java/com/fasterxml/classmate/members/RawMethodTest.java +++ b/src/test/java/com/fasterxml/classmate/members/RawMethodTest.java @@ -12,9 +12,8 @@ /** * @author blangel */ -@SuppressWarnings("deprecation") -public class RawMethodTest { - +public class RawMethodTest +{ private static class ModifiersClass { private strictfp void strictfpMethod() { } private native void nativeMethod(); @@ -91,8 +90,9 @@ public void createKey() { assertNotNull(methodKey); } + @SuppressWarnings("unlikely-arg-type") @Test - public void equals() { + public void testEquals() { // referential equality RawMethod rawMethod = new RawMethod(ResolvedObjectType.create(Object.class, null, null, null), toStringMethod); assertTrue(rawMethod.equals(rawMethod)); diff --git a/src/test/java/com/fasterxml/classmate/members/ResolvedFieldTest.java b/src/test/java/com/fasterxml/classmate/members/ResolvedFieldTest.java index 34d6c5f..5997366 100644 --- a/src/test/java/com/fasterxml/classmate/members/ResolvedFieldTest.java +++ b/src/test/java/com/fasterxml/classmate/members/ResolvedFieldTest.java @@ -9,9 +9,8 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; -@SuppressWarnings("deprecation") -public class ResolvedFieldTest { - +public class ResolvedFieldTest +{ @SuppressWarnings("unused") private static class ModifiersClass { private static String test; diff --git a/src/test/java/com/fasterxml/classmate/members/ResolvedMemberTest.java b/src/test/java/com/fasterxml/classmate/members/ResolvedMemberTest.java index 3555fc9..60aafc5 100644 --- a/src/test/java/com/fasterxml/classmate/members/ResolvedMemberTest.java +++ b/src/test/java/com/fasterxml/classmate/members/ResolvedMemberTest.java @@ -17,9 +17,8 @@ /** * @author blangel */ -@SuppressWarnings("deprecation") -public class ResolvedMemberTest { - +public class ResolvedMemberTest +{ @Retention(RetentionPolicy.RUNTIME) private static @interface Decorate { } diff --git a/src/test/java/com/fasterxml/classmate/members/ResolvedMethodTest.java b/src/test/java/com/fasterxml/classmate/members/ResolvedMethodTest.java index cd3e726..37c4090 100644 --- a/src/test/java/com/fasterxml/classmate/members/ResolvedMethodTest.java +++ b/src/test/java/com/fasterxml/classmate/members/ResolvedMethodTest.java @@ -12,8 +12,8 @@ /** * @author blangel */ -@SuppressWarnings("deprecation") -public class ResolvedMethodTest { +public class ResolvedMethodTest +{ private static abstract class ModifiersClass { private strictfp void strictfpMethod() { } private native void nativeMethod(); diff --git a/src/test/java/com/fasterxml/classmate/types/ResolvedArrayType51Test.java b/src/test/java/com/fasterxml/classmate/types/ResolvedArrayType51Test.java new file mode 100644 index 0000000..2ae1cc6 --- /dev/null +++ b/src/test/java/com/fasterxml/classmate/types/ResolvedArrayType51Test.java @@ -0,0 +1,18 @@ +package com.fasterxml.classmate.types; + +import com.fasterxml.classmate.BaseTest; +import com.fasterxml.classmate.ResolvedType; +import com.fasterxml.classmate.TypeResolver; + +public class ResolvedArrayType51Test extends BaseTest +{ + protected final TypeResolver RESOLVER = new TypeResolver(); + + // [classmate#51]: parent type for Array classes + public void testResolvingRawType() { + ResolvedType rt = RESOLVER.resolve(Long[].class); + ResolvedType parent = rt.getParentClass(); + assertNotNull(parent); + assertEquals(Object.class, parent.getErasedType()); + } +} diff --git a/src/test/java/com/fasterxml/classmate/types/ResolvedArrayTypeTest.java b/src/test/java/com/fasterxml/classmate/types/ResolvedArrayTypeTest.java index d1dc6b9..efb8384 100644 --- a/src/test/java/com/fasterxml/classmate/types/ResolvedArrayTypeTest.java +++ b/src/test/java/com/fasterxml/classmate/types/ResolvedArrayTypeTest.java @@ -7,12 +7,11 @@ import java.util.Collection; -import static junit.framework.Assert.*; +import static org.junit.Assert.*; /** * User: blangel */ -@SuppressWarnings("deprecation") public class ResolvedArrayTypeTest { @Test @@ -33,7 +32,9 @@ public void canCreateSubtypes() { @Test public void getParentClass() { ResolvedArrayType arrayType = new ResolvedArrayType(Object.class, null, null); - assertNull(arrayType.getParentClass()); + // With [classmate#51]: + assertNotNull(arrayType.getParentClass()); + assertEquals(Object.class, arrayType.getParentClass().getErasedType()); } @Test diff --git a/src/test/java/com/fasterxml/classmate/types/ResolvedObjectTypeTest.java b/src/test/java/com/fasterxml/classmate/types/ResolvedObjectTypeTest.java index ec42f87..2cc3b52 100644 --- a/src/test/java/com/fasterxml/classmate/types/ResolvedObjectTypeTest.java +++ b/src/test/java/com/fasterxml/classmate/types/ResolvedObjectTypeTest.java @@ -61,24 +61,25 @@ public void testGetStaticFields() { int count = staticFields.size(); switch (count) { - case 3: // Java 6 + case 3: // Java 8 matchRawMembers(staticFields, new String[] { "serialVersionUID", "serialPersistentFields", "CASE_INSENSITIVE_ORDER" }); break; - case 4: // Java 7/8 + case 6: // Java 9 matchRawMembers(staticFields, new String[] { - "serialVersionUID", "serialPersistentFields", "CASE_INSENSITIVE_ORDER", "HASHING_SEED" + "serialVersionUID", "serialPersistentFields", "CASE_INSENSITIVE_ORDER", + "COMPACT_STRINGS", "LATIN1", "UTF16" }); break; - case 6: // Java 9 + case 7: // Java 17 matchRawMembers(staticFields, new String[] { - "serialVersionUID", "serialPersistentFields", "CASE_INSENSITIVE_ORDER", + "serialVersionUID", "serialPersistentFields", "REPL", "CASE_INSENSITIVE_ORDER", "COMPACT_STRINGS", "LATIN1", "UTF16" }); break; default: - fail("Expected 3 (JDK 1.6), 4 (1.7/1.8) or 6 (1.9) static fields, got "+count+"; fields: "+staticFields); + fail("Expected 4 (1.7/1.8), 7 (Java 9/11/14) or 7 (17+) static fields, got "+count+"; fields: "+staticFields); } } diff --git a/src/test/java/com/fasterxml/classmate/types/ResolvedPrimitiveTypeTest.java b/src/test/java/com/fasterxml/classmate/types/ResolvedPrimitiveTypeTest.java index 5f8090c..4119744 100644 --- a/src/test/java/com/fasterxml/classmate/types/ResolvedPrimitiveTypeTest.java +++ b/src/test/java/com/fasterxml/classmate/types/ResolvedPrimitiveTypeTest.java @@ -4,9 +4,8 @@ import static junit.framework.Assert.*; -@SuppressWarnings("deprecation") -public class ResolvedPrimitiveTypeTest { - +public class ResolvedPrimitiveTypeTest +{ private static interface Callback { void test(ResolvedPrimitiveType primitiveType); } diff --git a/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveTypeTest.java b/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveTypeTest.java index 3865272..57a5540 100644 --- a/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveTypeTest.java +++ b/src/test/java/com/fasterxml/classmate/types/ResolvedRecursiveTypeTest.java @@ -4,9 +4,8 @@ import static junit.framework.Assert.*; -@SuppressWarnings("deprecation") -public class ResolvedRecursiveTypeTest { - +public class ResolvedRecursiveTypeTest +{ private static abstract class AbstractClass { } @SuppressWarnings("unused") diff --git a/src/test/java/com/fasterxml/classmate/types/TypePlaceHolderTest.java b/src/test/java/com/fasterxml/classmate/types/TypePlaceHolderTest.java index 3b59bf2..daaa857 100644 --- a/src/test/java/com/fasterxml/classmate/types/TypePlaceHolderTest.java +++ b/src/test/java/com/fasterxml/classmate/types/TypePlaceHolderTest.java @@ -4,9 +4,8 @@ import static junit.framework.Assert.*; -@SuppressWarnings("deprecation") -public class TypePlaceHolderTest { - +public class TypePlaceHolderTest +{ @Test public void canCreateSubtypes() { TypePlaceHolder placeHolder = new TypePlaceHolder(0);