Always find interface annotations in findAllLocalMergedAnnotations()

Prior to this commit, AnnotationDescriptor's
findAllLocalMergedAnnotations() filtered results using
MergedAnnotationPredicates.firstRunOf(
MergedAnnotation::getAggregateIndex), which retains only annotations
that share the same aggregate index as the first annotation
encountered. Since annotations on the root declaring class have
aggregate index 0 and annotations on interfaces have higher indices,
interface annotations were silently excluded whenever the root
declaring class itself declared the annotation.

This commit fixes the issue by replacing the aggregate-index-based
filter with a source-based filter that explicitly includes annotations
from the root declaring class and from each directly implemented
interface. The Javadoc for findAllLocalMergedAnnotations() has also
been updated to document the ordering guarantee: annotations from the
root declaring class appear first, followed by annotations from
implemented interfaces in declaration order.

Closes gh-36975
This commit is contained in:
Sam Brannen
2026-06-28 14:35:18 +02:00
parent efaa53b488
commit ce718cf699
2 changed files with 123 additions and 9 deletions
@@ -274,6 +274,73 @@ class TestContextAnnotationUtilsTests {
}
@Nested
@DisplayName("findAllLocalMergedAnnotations() tests")
class FindAllLocalMergedAnnotationsTests {
@Test
void annotationOnClassOnly() {
var descriptor = findAnnotationDescriptor(AnnotatedClass.class, ContextConfiguration.class);
assertThat(descriptor).isNotNull();
assertThat(descriptor.findAllLocalMergedAnnotations())
.singleElement()
.satisfies(config -> assertThat(config.classes()).containsExactly(Config1.class));
}
@Test
void annotationOnInterfaceOnly() {
var descriptor = findAnnotationDescriptor(ClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
assertThat(descriptor).isNotNull();
assertThat(descriptor.findAllLocalMergedAnnotations())
.singleElement()
.satisfies(config -> assertThat(config.classes()).containsExactly(Config2.class));
}
@Test
void annotationOnClassAndInterface() {
var descriptor = findAnnotationDescriptor(AnnotatedClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
assertThat(descriptor).isNotNull();
assertThat(descriptor.findAllLocalMergedAnnotations())
.satisfiesExactly(
config1 -> assertThat(config1.classes()).containsExactly(Config1.class),
config2 -> assertThat(config2.classes()).containsExactly(Config2.class)
);
}
@Test
void annotationOnTwoInterfaces() {
var descriptor = findAnnotationDescriptor(ClassImplementingTwoAnnotatedInterfaces.class, ContextConfiguration.class);
assertThat(descriptor).isNotNull();
assertThat(descriptor.findAllLocalMergedAnnotations())
.satisfiesExactly(
config1 -> assertThat(config1.classes()).containsExactly(Config2.class),
config2 -> assertThat(config2.classes()).containsExactly(Config3.class)
);
}
@Test
void annotationOnSuperclassIsExcluded() {
var descriptor = findAnnotationDescriptor(SubAnnotatedClass.class, ContextConfiguration.class);
assertThat(descriptor).isNotNull();
assertThat(descriptor.getRootDeclaringClass()).isEqualTo(AnnotatedClass.class);
assertThat(descriptor.findAllLocalMergedAnnotations())
.singleElement()
.satisfies(config -> assertThat(config.classes()).containsExactly(Config1.class));
}
@Test
void metaAnnotationOnClassAndAnnotationOnInterface() {
var descriptor = findAnnotationDescriptor(MetaAnnotatedClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
assertThat(descriptor).isNotNull();
assertThat(descriptor.findAllLocalMergedAnnotations())
.satisfiesExactly(
config1 -> assertThat(config1.classes()).containsExactly(DevConfig.class, ProductionConfig.class),
config2 -> assertThat(config2.classes()).containsExactly(Config2.class)
);
}
}
@Nested
@DisplayName("findAnnotationDescriptorForTypes() tests")
class FindAnnotationDescriptorForTypesTests {
@@ -377,7 +444,7 @@ class TestContextAnnotationUtilsTests {
assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType);
assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEmpty();
assertThat(((ContextConfiguration) descriptor.getAnnotation()).classes())
.containsExactly(MetaConfig.DevConfig.class, MetaConfig.ProductionConfig.class);
.containsExactly(DevConfig.class, ProductionConfig.class);
}
@Test
@@ -559,12 +626,12 @@ class TestContextAnnotationUtilsTests {
@AliasFor(annotation = ContextConfiguration.class)
Class<?>[] classes() default { DevConfig.class, ProductionConfig.class };
class DevConfig {
}
}
class ProductionConfig {
}
static class DevConfig {
}
static class ProductionConfig {
}
// -------------------------------------------------------------------------
@@ -668,4 +735,45 @@ class TestContextAnnotationUtilsTests {
}
}
// -------------------------------------------------------------------------
// Fixtures for FindAllLocalMergedAnnotationsTests
static class Config1 {
}
static class Config2 {
}
static class Config3 {
}
@ContextConfiguration(classes = Config2.class)
interface AnnotatedInterface {
}
@ContextConfiguration(classes = Config3.class)
interface AnotherAnnotatedInterface {
}
@ContextConfiguration(classes = Config1.class)
static class AnnotatedClass {
}
static class SubAnnotatedClass extends AnnotatedClass {
}
static class ClassImplementingAnnotatedInterface implements AnnotatedInterface {
}
@ContextConfiguration(classes = Config1.class)
static class AnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface {
}
static class ClassImplementingTwoAnnotatedInterfaces implements AnnotatedInterface, AnotherAnnotatedInterface {
}
@MetaConfig
static class MetaAnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface {
}
}