0

I am having a null analysis issue when compiling GriefPrevention in Eclipse. For example, line 304 in PlayerEventHandler.java invokes Player#getLocation(). The API specification for this class can be found here.

The problem is that this class inherits from both the Entity and OfflinePlayer interfaces, which have conflicting annotations.

For Entity, getLocation() is defined as NonNull:

@NotNull Location getLocation(); 

But for OfflinePlayer, it is defined as Nullable:

/** * Gets the player's current location. * * @return the player's location, {@code null} if player hasn't ever played * before. */ @Nullable public Location getLocation(); 

It's my opinion that, given this situation, the least permissive annotation (Nullable) should be given precedence. However, IntelliJ disagrees. This creates a problem, because I cannot compile this code with Eclipse without disabling null annotations, which is undesirable, and the developer who uses IntelliJ insists that this is not a problem (according to his IDE).

Is either IntelliJ or Eclipse's interpretation "more correct" or is the result ambiguous given the conflicting annotation in the API specification?

When testing this kind of ambiguous inheritance, IntelliJ allows @NotNull without any warning or error, but @Nullable throws a warning:

import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class Main { interface ITestNotNull { @NotNull Object ambiguous(); } interface ITestNullable { @Nullable Object ambiguous(); } static class ITest implements ITestNullable, ITestNotNull { @Override // Method annotated with @Nullable must not override @NotNull method public @Nullable Object ambiguous() { return new Object(); } } public static void main(String[] args) { ITest a = new ITest(); System.out.println(a.ambiguous()); } } 

The opposite is true in Eclipse, however I receive a compiler error instead:

import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; public class Main { interface ITestNotNull { @NonNull Object ambiguous(); } interface ITestNullable { @Nullable Object ambiguous(); } static class ITest implements ITestNullable, ITestNotNull { @Override // The return type is incompatible with '@NonNull Object' returned from // Main.ITestNotNull.ambiguous() (mismatching null constraints) public @Nullable Object ambiguous() { return new Object(); } } public static void main(String[] args) { ITest a = new ITest(); System.out.println(a.ambiguous()); } } 

UPDATE

Upon further consideration and analysis of the problem, the issue I have been experience seems to be a bug in Eclipse. IntelliJ's implementation of @NonNull seems to be correct. Regardless of the ambiguity, it satisfies both possible implementations.

https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2308

My original report was under the false premise that the annotation behavior experienced when importing a jar was correct and that the native source-level implementation was wrong. The opposite is true. There is nothing wrong with the source-level implementation; this bug can only be demonstrated when importing a jar file (see the git bug report).

Here is a very simple example demonstrating the actual bug:

import java.util.Objects; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; public class Main { public static void main(String[] args) { @Nullable Object nullableObject; @NonNull Object nonNullObject; Player player = Objects.requireNonNull(Bukkit.getPlayer("")); /* Null annotation error *WRONGLY* derives the @Nullable implementation when choosing between the two conflicting interfaces */ // Unexpected inverse @Nullable implementation is derived // nonNullObject = player.getLocation(); // ^^^^^^^^^^^^^^^^^^^^ Error: Null type mismatch // This is the expected @NonNull interface implementation nonNullObject = ((Entity)player).getLocation(); // This would be a NPE if Player was actually implementing @Nullable! nullableObject = ((OfflinePlayer)player).getLocation(); } } 

Spigot API jar library with source:
spigot-api-1.20.4-R0.1-20240407.022953-115.jar
spigot-api-1.20.4-R0.1-20240407.022953-115-sources.jar

Eclipse External Annotations (EEA) for Spigot API
spigot-api-eea.zip

6
  • It is not ambiguous, but a conflict, because a method cannot return something that is both, @NonNull and @Nullable. Eclipse is right (mismatching null constraints) and IntelliJ is not smart enough to detect this. IntelliJ completely misses this issue and only detects contradictions of overriding and overridden method annotations, which is different thing (but also based on the fact, that something cannot be @NonNull and @Nullable). Commented Apr 8, 2024 at 7:51
  • It's easy to want to praise Eclipse here, but just like IntelliJ, if the opposite annotation @NonNull is used, the error disappears. Each IDE believes that one is "correct", however, it happens to be the inverse of the other. Commented Apr 8, 2024 at 15:31
  • Your easy praising is an easy-peasy straw man. The point is that ambiguous() is not ambiguous, but that there are clearly conflicting constraints: something cannot be @NonNull and @Nullable. ITest implements ITestNullable, ITestNotNull means "ITest implements ITestNullable and ITestNotNull" and that it must be ensured that ITest can be used as ITestNullable as well as ITestNotNull. If each IDE misses one of the two cases, then both are wrong, which is pretty much the same thing, not the opposite. A null annotation is a constraint that cannot be overwritten. Commented Apr 9, 2024 at 5:59
  • @howlger I have updated my question. This appears to be a bug in Eclipse that manifests only when encountering this particular conflict from dependencies derived from jar files. Commented Apr 10, 2024 at 7:25
  • See my answer below with an example that proves the opposite. Commented Apr 10, 2024 at 13:15

2 Answers 2

1

In ecj, annotation based null analysis piggy-backs on compilation proper. With that the primary rule is to comply with JLS. In this example the compiler entered an area, where JLS 15.12.2.5 mandates that the method invocation selects a method from several equivalent methods by arbitrary choice. After that selection null analysis could either find a @Nullable or a @NonNull return type.

NB: Aside from the undesired compiler warning, the ambiguity is harmless for two reasons: From a pure Java p.o.v. null annotations are irrelevant for compilation as well as for execution. Secondly, any class implementing an interface like Player is forced (by the rules of null analysis) to respect both contracts, in which case the stronger contract, viz to return a non-null value, wins.

Luckily the leeway given by the specification ("chosen arbitrarily") could be used to improve null analysis in ecj. With that fix callers of Player.getLocation() will see the stronger of both contracts, and can thus leverage the promise that non-null will be returned.

Thanks to @Zhro and @howlger for reporting the issue and helping in its analysis.

Sign up to request clarification or add additional context in comments.

Comments

-2

The inverse, if you like, of Nullable is NonNull which is part of a completely unrelated annotation processor than NotNull, so strictly speaking you can have both and there would be no contradiction.

3 Comments

The issue is with the inverse behavior between the two annotation processors, not the annotation itself. In this case, all annotations are org.jetbrains.annotations, but are being evaluated differently by the Eclipse annotation analysis.
You should configure both IDE to run the same annotation processors with the same options (i.e. jetbrains.com/help/idea/annotation-processors-support.html)
I think you misunderstand. The annotations are being processed. The null analysis and introspection is handled by the IDE. Each environment handles this differently; in this case, inversely.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.