Search code examples
javaeclipselanguage-lawyerjava-17jsr305

@Nullable annotation cannot be applied here


Eclipse shows this as a Java problem:

Annotation types that do not specify explicit target element types cannot be applied here.

This only happened when I upgraded from Eclipse 2022-09 to 2023-09.

This is an example when this happens:

public <T> @Nullable Set<T> getSomeSet(T something) {
  // ...
}

The annotation is the javax.annotation.Nullable from the jsr305 library.

I'm using Adoptium OpenJDK 17.0.8.101-hotspot.

  • This didn't happen with Eclipse 2022-09.
  • This doesn't happen with IntelliJ.
  • This doesn't happen when I compile via gradlew compileJava.
  • This doesn't happen with public @Nullable Set<?> getSomeSet() { ... }
  • This doesn't happen with public @Nullable <T> Set<T> getSomeSet(T something)

I don't understand why is that happening in this specific circumstance now (and not in all other cases) and what can I do to make that error disappear from my Eclipse IDE. I didn't find any option to disable this kind of error in the preferences.

EDIT: Simplified the minimal reproduction case and added "doesn't happen" case #4.

EDIT2: Added "doesn't happen" case #5.


Solution

  • It may seem as if the declarations

    public <T> @Nullable Set<T> getSomeSet(T something)
    public @Nullable <T> Set<T> getSomeSet(T something)
    

    are syntactically different, i.e. that in the first variant, the annotation is bound to the return type. Apparently, Eclipse follows this idea.

    The grammar of the specification, however, shows both as part of the method declaration:

    MethodDeclaration:
        {MethodModifier} MethodHeader MethodBody
    MethodHeader:
        Result MethodDeclarator [Throws]
        TypeParameters {Annotation} Result MethodDeclarator [Throws]
    MethodDeclarator:
        Identifier ( [ReceiverParameter ,] [FormalParameterList] ) [Dims]
    

    MethodModifier

    MethodModifier:
        (one of)
        Annotation public protected private
        abstract static final synchronized native strictfp
    

    Note that {Annotation} between TypeParameters and Result is part of the MethodHeader.

    Then the Java Language Specification, §9.7.4 says:

    It is possible for an annotation to appear at a syntactic location in a program where it could plausibly apply to a declaration, or a type, or both.

    [...]

    The grammar of the Java programming language unambiguously treats annotations at these locations as modifiers for a declaration (§8.3), but that is purely a syntactic matter. Whether an annotation applies to the declaration or to the type of the declared entity - and thus, whether the annotation is a declaration annotation or a type annotation - depends on the applicability of the annotation's interface […]

    The way I read this, is that the syntactic location shouldn’t matter, but only the applicability of annotation, given by the @Target annotation or its absence. That’s also, how javac handles this, unlike Eclipse:

    public class AnnotationExample {
        public static void main(String[] args) throws NoSuchMethodException {
            Method m = AnnotationExample.class.getMethod("methodName");
            System.out.println("method:      " + Arrays.toString(m.getAnnotations()));
            System.out.println("return type: "
                + Arrays.toString(m.getAnnotatedReturnType().getAnnotations()));
        }
    
        @Retention(RetentionPolicy.RUNTIME) @Target({TYPE_USE, METHOD}) @interface A {}
        @Retention(RetentionPolicy.RUNTIME) @Target({TYPE_USE, METHOD}) @interface B {}
    
        public static @A <T extends Enum<T>> @B Set<T> methodName() {
            return null;
        }
    }
    

    When compiled with javac, it will print

    method:      [@AnnotationExample$A(), @AnnotationExample$B()]
    return type: [@AnnotationExample$A(), @AnnotationExample$B()]
    

    but when compiled with Eclipse, it prints

    method:      [@AnnotationExample.A()]
    return type: [@AnnotationExample.A(), @AnnotationExample.B()]
    

    indicating that Eclipse treats the annotation behind the type parameter declaration as an annotation of the return type only. Consequently, changing B’s declaration to @Target(METHOD) produces a compile error in Eclipse.


    This behavior interacts with the targets assumed to be valid when no @Target annotation has been given:

    From Java 8 to Java 13, the specification looked like

    If an annotation of type java.lang.annotation.Target is not present on the declaration of an annotation type T, then T is applicable in all declaration contexts except type parameter declarations, and in no type contexts.

    Starting with Java 17, the specification looks like

    If an annotation of type java.lang.annotation.Target is not present on the declaration of an annotation interface A, then A is applicable in all declaration contexts and in no type contexts.

    So, this looks like a straight-forward change, now also permitting type parameter declarations as target when no @Target has been given, which does not affect our scenario.

    If there weren’t the in-between versions, 14 to 16:

    If an annotation of type java.lang.annotation.Target is not present on the declaration of an annotation interface A, then A is applicable in all nine declaration contexts and in all 16 type contexts.

    (in Java 16, there are ten declaration contexts and 17 type contexts)

    This directly affects compilers assuming that the annotation following the type parameter declaration are type contexts only. And since this is more permissive than the subsequent version, it’s understandable if a compiler vendor gets surprised by this and has to deliver the more restrictive behavior in an update.

    (Note that javac dodged this by not implementing these changes in the first place, JDK 21 is the first to implement the behavior specified since Java 17, see JDK-8309743)


    It’s important to keep in mind that while the compiler error shouldn’t happen, the specification changes will still affect what is reported to be annotated when inspecting the code. When you want to be able to annotate types, rather than the method, you need an explicit @Target including TYPE_USE.