Skip to content

AnnotationDescriptor.findAllLocalMergedAnnotations fails to find interface annotations #36975

Description

@nosan

The Javadoc states:

Find all annotations of the specified annotation type that are present or meta-present on the root declaring class of this descriptor or on any interfaces that the root declaring class implements.

However, AnnotationDescriptor.findAllLocalMergedAnnotations appears to ignore annotations declared on implemented interfaces. My understanding is that the method should return annotations declared directly on the class together with those from its implemented interfaces, while excluding annotations from superclasses.

Instead, the current implementation only returns the annotations declared directly on the class.

sample.zip

Activity

  1. self-assigned this
    on Jun 28, 2026
  2. added and removed on Jun 28, 2026
  3. added this to the 7.1.0-M1 milestone on Jun 28, 2026
  4. sbrannen commented on Jun 28, 2026

    @sbrannen
    Member

    Hi @nosan,

    Good catch, and thanks for reporting this! 👍

    My understanding is that the method should return annotations declared directly on the class together with those from its implemented interfaces, while excluding annotations from superclasses.

    Indeed, that was the original intent.

    However, AnnotationDescriptor.findAllLocalMergedAnnotations appears to ignore annotations declared on implemented interfaces.

    That's partially true. Annotations are found on interfaces if there are none found on the root declaring class. However, you're right: interface annotations are currently silently excluded whenever the root declaring class itself declares the annotation.

    Since this is a long standing bug whose fix technically introduces a regression in behavior, I will only apply the fix to 7.1.

    Regards,

    Sam

  5. added a commit that references this issue on Jun 28, 2026
    ce718cf
  6. nosan commented on Jun 28, 2026

    @nosan
    ContributorAuthor

    Hi @sbrannen,

    Thank you very much for the quick fix!

    I have a question regarding this change, since it appears to alter the existing behavior. For example:

    static class First implements Second {
    
    }
    
    interface Second extends Third {
    
    }
    
    @TypeExcludeFilters(ThirdSliceExclude.class)
    interface Third {
    
    }

    Before this fix, the following test passed:

    AnnotationDescriptor<TypeExcludeFilters> descriptor =
            TestContextAnnotationUtils.findAnnotationDescriptor(First.class, TypeExcludeFilters.class);
    
    assertThat(descriptor).isNotNull();
    assertThat(descriptor.findAllLocalMergedAnnotations()).hasSize(1);

    With the new changes, this is no longer the case.

    Based on my understanding of AnnotationDescriptor.findAnnotationDescriptor() and the implementation of AnnotationDescriptor.next(), I thought the original intention was to locate the first class in the hierarchy that declares the annotation. Once that "root declaring class" is found, findAllLocalMergedAnnotations() would return all matching annotations declared on that class as well as on its implemented interfaces, while intentionally excluding annotations from superclasses. The superclass would then become the root declaring class for the next AnnotationDescriptor returned by next().

    In other words, the flow would be:

    Find the first class in the hierarchy that declares the annotation.

    • Use findAllLocalMergedAnnotations() to retrieve all relevant annotations associated with that root declaring class (including its interfaces).
    • Call next() to move to the next root declaring class higher in the hierarchy, if needed.

    If that understanding is correct, perhaps the original implementation was intended to behave more like this:

    private Set<T> findAllLocalMergedAnnotations() {
        return Stream.concat(getDirectAnnotations(), getInterfaceAnnotations())
                  .collect(MergedAnnotationCollectors.toAnnotationSet());
    
    }
    
    private Stream<MergedAnnotation<T>> getDirectAnnotations() {
        return MergedAnnotations.search(SearchStrategy.DIRECT)
                .from(getRootDeclaringClass())
                .stream(getAnnotationType());
    }
    
    private Stream<MergedAnnotation<T>> getInterfaceAnnotations() {
        return Arrays.stream(getRootDeclaringClass().getInterfaces())
                .flatMap(ifc -> MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
                        .from(ifc)
                        .stream(getAnnotationType()));
    }

    Or am I misunderstanding the original design of AnnotationDescriptor?

  7. sbrannen commented on Jun 30, 2026

    @sbrannen
    Member

    Hi @nosan,

    Thanks a lot for reviewing the changes and discovering a regression.

    I had indeed overlooked the extended interfaces scenario, and I am reopening this issue to address that similar to the approach you have outlined.

    Cheers

    Sam

  8. added a commit that references this issue on Jun 30, 2026
    c5d7908
  9. nosan commented on Jun 30, 2026

    @nosan
    ContributorAuthor

    Thank you very much, @sbrannen 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

in: testIssues in the test moduletype: bugA general bug

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions