У меня возникла проблема с нулевым анализом при компиляции GriefPrevention в Eclipse. Например, строка 304 в PlayerEventHandler.java вызывает Player#getLocation(). Спецификацию API для этого класса можно найти здесь.
Проблема в том, что этот класс наследует интерфейсы Entity и OfflinePlayer, которые имеют конфликтующие аннотации.
Для Entity getLocation() определяется как NonNull:
@NotNull
Location getLocation();
Но для OfflinePlayer он определяется как 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();
По моему мнению, в этой ситуации наименее разрешительная аннотация (Nullable) должна иметь приоритет. Однако IntelliJ с этим не согласен. Это создаёт проблему, поскольку я не могу скомпилировать этот код с помощью Eclipse, не отключив нулевые аннотации, что нежелательно, а разработчик, использующий IntelliJ, уверяет, что это не проблема (согласно его IDE).
Является ли интерпретация IntelliJ или Eclipse «более правильной» или результат неоднозначен, учитывая противоречивую аннотацию в спецификации API?
При тестировании такого рода неоднозначного наследования IntelliJ позволяет @NotNull без каких-либо предупреждений или ошибок, но @Nullable выдает предупреждение:
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());
}
}
В Eclipse верно обратное, однако вместо этого я получаю ошибку компилятора:
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());
}
}
При дальнейшем рассмотрении и анализе проблемы выяснилось, что проблема, с которой я столкнулся, связана с ошибкой в Eclipse. Реализация @NonNull в IntelliJ кажется правильной. Независимо от неоднозначности, он удовлетворяет обеим возможным реализациям.
https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2308
Мой первоначальный отчет основывался на ложной предпосылке, что поведение аннотаций при импорте jar было правильным, а собственная реализация на уровне исходного кода была неправильной. Верно обратное. В реализации на уровне исходного кода нет ничего плохого; эту ошибку можно продемонстрировать только при импорте файла jar (см. отчет об ошибках git).
Вот очень простой пример, демонстрирующий реальную ошибку:
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();
}
}
Библиотека jar Spigot API с исходным кодом:
spigot-api-1.20.4-R0.1-20240407.022953-115.jar
spigot-api-1.20.4-R0.1-20240407.022953-115-sources.jar
Внешние аннотации Eclipse (EEA) для Spigot API
spigot-api-eea.zip
Здесь легко похвалить Eclipse, но, как и в случае с IntelliJ, если используется противоположная аннотация @NonNull, ошибка исчезает. Каждая IDE считает, что один из них «правильный», однако он оказывается обратным другому.
Ваша легкая похвала – это легкий соломенный чувак. Дело в том, что ambiguous() не является двусмысленным, но существуют явно противоречивые ограничения: что-то не может быть @NonNull и @Nullable. ITest implements ITestNullable, ITestNotNull означает «ITest реализует ITestNullable и ITestNotNull», и необходимо гарантировать, что ITest можно использовать как ITestNullable, так и ITestNotNull. Если каждая IDE пропускает один из двух случаев, то оба неверны, что практически одно и то же, а не наоборот. Нулевая аннотация — это ограничение, которое нельзя перезаписать.
@howlger Я обновил свой вопрос. Похоже, это ошибка в Eclipse, которая проявляется только при возникновении этого конкретного конфликта из-за зависимостей, полученных из файлов jar.
Смотрите мой ответ ниже с примером, доказывающим обратное.
@howlger Ваш ответ не учитывает файлы jar, именно здесь проявляется ошибка.




Инверсией, если хотите, Nullable является NonNull, который является частью совершенно не связанного с NotNull процессора аннотаций, поэтому, строго говоря, вы можете использовать оба, и не будет никакого противоречия.
Проблема заключается в обратном поведении между двумя обработчиками аннотаций, а не в самой аннотации. В этом случае все аннотации имеют статус org.jetbrains.annotations, но анализом аннотаций Eclipse они оцениваются по-разному.
Вам следует настроить обе IDE для запуска одних и тех же обработчиков аннотаций с одинаковыми параметрами (т. е. jetbrains.com/help/idea/annotation-processors-support.html).
Я думаю, вы неправильно понимаете. Аннотации обрабатываются. Нулевой анализ и самоанализ выполняются IDE. Каждая среда обрабатывает это по-своему; в данном случае наоборот.
В ecj нулевой анализ на основе аннотаций применяется непосредственно к компиляции. При этом основным правилом является соблюдение JLS. В этом примере компилятор ввел область, где JLS 15.12.2.5 требует, чтобы вызов метода выбирал метод из нескольких эквивалентных методов произвольным выбором. После этого нулевой анализ выбора может найти возвращаемый тип @Nullable или @NonNull.
NB: Если не считать нежелательного предупреждения компилятора, двусмысленность безвредна по двум причинам: Из чистой Java p.o.v. Нулевые аннотации не имеют значения ни для компиляции, ни для выполнения. Во-вторых, любой класс, реализующий такой интерфейс, как Player, вынужден (по правилам нулевого анализа) соблюдать оба контракта, и в этом случае побеждает более сильный контракт, а именно возвращающий ненулевое значение.
К счастью, свободу действий, предоставляемую спецификацией («выбрано произвольно»), можно использовать для улучшения анализа нулей в ecj. Благодаря этому исправлению вызывающие Player.getLocation() будут видеть более сильный из обоих контрактов и, таким образом, смогут воспользоваться обещанием, что будет возвращено ненулевое значение.
Спасибо @Zhro и @howlger за сообщение о проблеме и помощь в ее анализе.
Это не двусмысленность, а конфликт, поскольку метод не может возвращать что-то одновременно и
@NonNullи@Nullable. Eclipse прав (mismatching null constraints), а IntelliJ недостаточно умен, чтобы это обнаружить. IntelliJ полностью упускает из виду эту проблему и обнаруживает только противоречия в аннотациях переопределения и переопределения метода, что совсем другое (но также основано на том факте, что что-то не может быть@NonNullи@Nullable).