smallrye / jandex

Java Annotation Indexer

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Uniform API for accessing annotations from AnnotationTarget

Ladicek opened this issue · comments

Two PRs (#64, #75) already attempt to make the annotation access API more regular. I think this should be addressed holistically. In my opinion, all the annotation access methods should be present on AnnotationTarget, so that all annotation targets expose uniform API. It would look roughly like this:

interface AnnotationTarget {
    ...

    boolean hasAnnotation(DotName name);
    AnnotationInstance annotation(DotName name);
    Collection<AnnotationInstance> annotationWithRepeatable(DotName name, IndexView index);
    Collection<AnnotationInstance> annotations();
    Map<DotName, List<AnnotationInstance>> annotationsMap();

    boolean hasDirectAnnotation(DotName name);
    AnnotationInstance directAnnotation(DotName name);
    Collection<AnnotationInstance> directAnnotationWithRepeatable(DotName name, IndexView index);
    Collection<AnnotationInstance> directAnnotations();
    Map<DotName, List<AnnotationInstance>> directAnnotationsMap();
}

The direct* methods would return annotations that are present directly on the annotation target, while the generic methods would -- just like now -- return annotations present on the annotation target as well as nested annotation targets.

This would unfortunately be a breaking change, because ClassInfo currently exposes a very different API than other annotation targets and there's a name collision.

+10 for unification.

annotationWithRepeatable() should be probably annotationsWithRepeatable() because it returns a collection. TBH I don't like the name very much. But the truth is that annotations(DotName repeatableName, IndexView index) could be quite confusing.

I wonder if it would make sense to replace the "direct" variants of the method with composition, i.e. something like AnnotationTarget direct() or AnnotationTarget directAnnotations().

Also I'm not 100% sure it makes sense to define both annotations() and annotationsMap(). I understand that the map is sometimes useful for ClassInfo and MethodInfo but the common use case "Is an annotation used somewhere on the class/method?" can be solved with hasAnnotation(DotName name) ...

I think annotationWithRepeatable was probably a typo, not an intentional change. Jandex 2.4 calls it annotationsWithRepeatable, so 3.0 should too. No reason to change that.

I didn't think of adding AnnotationTarget direct() before. It's an interesting idea, but I think it would cause more harm than good. Suddenly, you would have 2 different AnnotationTarget objects, both referring to the same declaration/type, but each returning different annotations. I thought having annotations() and directAnnotations() would be natural for users, as it's roughly similar to Reflection (e.g. Class.getMethods() vs. Class.getDeclaredMethods()). Not exactly happy about the total number of methods, but I think it's the best option.

And agree that annotationsMap() is superfluous. I added it to the proposal because ClassInfo currently presents its annotations in that form, but we could drop it for sure without loss of generality.

I just want to mention that AnnotationInstance annotation(DotName name) should likely return a Collection since it includes the nested instances. I think this is actually an issue with MethodInfo currently.

MethodInfo and FieldInfo (and RecordComponentInfo) have that problem. ClassInfo.classAnnotation probably doesn't, as that doesn't return nested annotations.

I need to figure out a better name for the direct* variants, because I realized that there's a notion of "directly" and "indirectly" present annotations on classes, and that's different from what I'm trying to do here. I originally thought I could use declared* to mimic reflection, but that's probably bad too.

At the moment, I'm thinking element would be a good choice, because elementAnnotations is pretty clear in that it returns annotations only on an element (and not on other elements, nested in it). Problem is that Jandex doesn't use the term element anywhere yet :-)

(For completeness, I'll add that yet another option would be to make annotations return only annotations on given annotation target, and what's currently called annotations would be renamed to something like annotationsWithNested. But that kind of behavioral breakage is something I'm not willing to undertake.)

I started looking into this for good and there's a couple places in the Jandex API where the behavior is not exactly deterministic, Obviously, the prime example is the existing annotation(DotName) method on MethodInfo and FieldInfo (and RecordComponentInfo) [1]. Here's a simple example:

@MyAnnotation("f1")
List<@MyAnnotation("f2") String> field;

@MyAnnotation("m1")
void method(@MyAnnotation("m2") List<@MyAnnotation("m3") String> param) {
}

In this case, MyAnnotation may be present on any target, including type usages, so field has 3 occurences and method has 4.

Obtaining a FieldInfo for field or MethodInfo for method and calling annotation(DotName.createSimple(MyAnnotation.class.getName())) simply returns the first matching annotation the binary search algorithm finds in the sorted array of annotations.

This would obviously call for removing this method, but I'm afraid it is used a lot. And works just fine when only one annotation of given name is present, which is the most common case. So instead of removing, I'm thinking I'll do this:

  1. document that the method is non-deterministic when the annotation occurs more than once on the element and anything nested in it;
  2. add an overload of existing annotations() method looking like this: Collection<AnnotationInstance> annotations(DotName).

[1] Another example is ClassInfo.method(String, Type...) method, because the JVM signature of a method includes its return type. It usually works as expected, because the most common situation where multiple methods are present that only differ in the return type is in case of bridge methods, and the method search prefers non-synthetic methods. It would still behave non-deterministically if there were multiple non-synthetic methods only differing in the return type.

I also just noticed that MethodInfo.annotationsWithRepeatable is not consistent with {Field,RecordComponent}Info.annotationsWithRepeatable in some subtle ways, and that should probably be rectified.

My original goal here is to break as little as possible, and if there's a breakage, it should be caught during compilation. This behavioral difference between annotationsWithRepeatable and a potential fix unfortunately can't be source/binary incompatible, so... will think about that.

Regarding your first example agree it should be documented although this was intentional it’s a short-hand “findFirst” to cut down boiler plate when the results are unambiguous.

WRT to the second example, JLS semantics do not allow multiple different return values for the same argument signature (yet JVM semantics do allow it), (exploited by bridge methods). Jandex originally was focused on just plain Java so in some cases precision was sacrificed to cut boilerplate. I

Regarding your first example agree it should be documented although this was intentional it’s a short-hand “findFirst” to cut down boiler plate when the results are unambiguous.

Hm, I personally think that deterministic or not the annotation() method should never be used for ambiguous results. On the other hand, it's useful if the annotation is not repeatable and the annotation target eliminates the ambiguity. That said, +1 for improving the javadoc ;-).

I actually found yesterday that MethodInfo already has an annotations(DotName) method, so I'll just add the same method to other annotation targets.

In case anyone is curious, here are my current additions to AnnotationTarget:

/**
 * Returns whether an annotation instance with given name is declared on this annotation target or any of its
 * nested annotation targets.
 *
 * @param name name of the annotation type to look for, must not be {@code null}
 * @return {@code true} if the annotation is present, {@code false} otherwise
 * @since 3.0
 * @see #annotation(DotName)
 * @see #annotations()
 */
boolean hasAnnotation(DotName name);

/**
 * Returns the annotation instance with given name declared on this annotation target or any of its nested
 * annotation targets. The {@code target()} method of the returned annotation instance may be used to determine
 * the exact location of the annotation instance.
 * <p>
 * In case an annotation with given name occurs more than once, the result of this method is not deterministic.
 * For such situations, {@link #annotations(DotName)} is preferable.
 *
 * @param name name of the annotation type to look for, must not be {@code null}
 * @return the annotation instance, or {@code null} if not found
 * @since 3.0
 * @see #annotations(DotName)
 */
AnnotationInstance annotation(DotName name);

/**
 * Returns the annotation instances with given name declared on this annotation target and nested annotation targets.
 * The {@code target()} method of the returned annotation instances may be used to determine the exact location
 * of the respective annotation instance.
 *
 * @param name name of the annotation type, must not be {@code null}
 * @return collection of annotation instances, never {@code null}
 * @see #annotationsWithRepeatable(DotName, IndexView)
 * @see #annotations()
 * @since 3.0
 */
Collection<AnnotationInstance> annotations(DotName name);

/**
 * Returns the annotation instances with given name declared on this annotation target and nested annotation targets.
 * The {@code target()} method of the returned annotation instances may be used to determine the exact location
 * of the respective annotation instance.
 * <p>
 * If the specified annotation is repeatable, the result also contains all values from the container annotation
 * instance. In this case, the {@link AnnotationInstance#target()} returns the target of the container annotation
 * instance.
 *
 * @param name name of the annotation type, must not be {@code null}
 * @param index index used to obtain the annotation type, must not be {@code null}
 * @return collection of annotation instances, never {@code null}
 * @throws IllegalArgumentException if the index does not contain the annotation type or if {@code name} does not
 *         identify an annotation type
 * @see #annotations()
 * @since 3.0
 */
Collection<AnnotationInstance> annotationsWithRepeatable(DotName name, IndexView index);

/**
 * Returns the annotation instances declared on this annotation target and nested annotation targets.
 * The {@code target()} method of the returned annotation instances may be used to determine the exact location
 * of the respective annotation instance.
 *
 * @return collection of annotation instances, never {@code null}
 * @since 3.0
 */
Collection<AnnotationInstance> annotations();

The implementations of AnnotationTarget, such as ClassInfo, MethodInfo or FieldInfo, will have the same javadoc, modified to be more accurate (e.g. instead of "on this annotation target and nested annotation targets", the javadoc on MethodInfo will say "on this this method, any of its parameters or any type within its signature") and sometimes include examples.

A second set of methods to only access annotations declared directly on the target and not on nested targets is yet to be added. I'm still not sure what would be the best naming :-)

One thing about the annotationsWithRepeatable(DotName, IndexView) method that I'd like to hear opinions about. Currently, Jandex has 2 implementations that usually work the same, but are different when it comes to edge cases.

I identified 2 differences:

  1. One implementation checks whether the passed IndexView is null right at the very beginning and throws an IAE if so. The other implementation doesn't check at all and assumes that it is never null (a NPE may later be thrown).

  2. A somewhat more important difference occurs when the annotation (say MyAnnotation) in question is repeatable (say the container type is called MyAnnotationList) and the inspected element has both the annotation and the container annotation. That is actually perfectly legal, it looks like this:

    @MyAnnotation(1)
    @MyAnnotationList({
        @MyAnnotation(2),
        @MyAnnotation(3)
    })
    class MyClass { ... }

    In this case, one implementation of annotationsWithRepeatable returns all 3 occurrences of MyAnnotation. The other implementation stops when it discovers that MyAnnotation is present and doesn't look for the possible container annotation (so it only returns 1).

My current plan is to unify all implementations of annotationsWithRepeatable on these choices:

  1. Check IndexView for null eagerly, throw IAE if null.
  2. Always look for the container annotation, even if the "plain" annotation is present.
  • Check IndexView for null eagerly, throw IAE if null.

+1

  • Always look for the container annotation, even if the "plain" annotation is present.

That's an interesting edge case. And I agree that we should always look for the container annotation as well. TBH I know about a few places in quarkus where this corner case is not handled "correctly" ;-).

Done in #182.