Fix enclosing class resolution with ClassFile API

Prior to this commit, the `ClassFile` based implementation of
`AnnotationMetadata` would rely on the `NestHost` class element to get
the enclosing class name for a nested class.
This approach works for bytecode emitted by Java11+, which aligns with
our Java17+ runtime policy. But there are cases where bytecode was not
emitted by a Java11+ compiler, such as Kotlin. In this case, the
`NestHost` class element is absent and we should instead use the
`InnerClasses` information to get it.

This commit makes use of `InnerClasses` to get the enclosing class name,
but still uses `NestHost` as a fallback for anonymous classes.

Fixes gh-36451
This commit is contained in:
Brian Clozel
2026-03-12 10:47:39 +01:00
parent 0269eb80da
commit cc5c7ba186
2 changed files with 19 additions and 4 deletions
@@ -195,7 +195,7 @@ final class ClassFileAnnotationMetadata implements AnnotationMetadata {
builder.accessFlags(flags);
}
case NestHostAttribute _ -> {
builder.enclosingClass(classModel.thisClass());
builder.enclosingClassFromNestHost(classModel.thisClass());
}
case InnerClassesAttribute innerClasses -> {
builder.nestMembers(currentClassName, innerClasses);
@@ -256,11 +256,16 @@ final class ClassFileAnnotationMetadata implements AnnotationMetadata {
this.accessFlags = accessFlags;
}
void enclosingClass(ClassEntry thisClass) {
void enclosingClassFromNestHost(ClassEntry thisClass) {
if (this.enclosingClassName != null) {
return;
}
String thisClassName = thisClass.name().stringValue();
int currentClassIndex = thisClassName.lastIndexOf('$');
this.enclosingClassName = ClassUtils.convertResourcePathToClassName(
thisClassName.substring(0, currentClassIndex));
if (currentClassIndex > 0) {
this.enclosingClassName = ClassUtils.convertResourcePathToClassName(
thisClassName.substring(0, currentClassIndex));
}
}
void superClass(Superclass superClass) {
@@ -280,6 +285,8 @@ final class ClassFileAnnotationMetadata implements AnnotationMetadata {
if (currentClassName.equals(innerClassName)) {
// the current class is an inner class
this.innerAccessFlags = classInfo.flags();
classInfo.outerClass().ifPresent(outerClass ->
this.enclosingClassName = ClassUtils.convertResourcePathToClassName(outerClass.name().stringValue()));
}
classInfo.outerClass().ifPresent(outerClass -> {
if (outerClass.name().stringValue().equals(currentClassName)) {
@@ -148,6 +148,14 @@ public abstract class AbstractAnnotationMetadataTests {
assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull();
}
@Test
void getEnclosingClassNameWhenNestedMemberClassReturnsImmediateEnclosingClass() {
assertThat(get(TestNestedMemberClass.TestMemberClassInnerClassA.class).getEnclosingClassName())
.isEqualTo(TestNestedMemberClass.class.getName());
assertThat(get(TestNestedMemberClass.TestMemberClassInnerClassA.TestMemberClassInnerClassAA.class).getEnclosingClassName())
.isEqualTo(TestNestedMemberClass.TestMemberClassInnerClassA.class.getName());
}
@Test
void getSuperClassNameWhenHasSuperClassReturnsName() {
assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName());