IntelliJ и Eclipse расходятся во мнениях по поводу нулевых аннотаций через несколько интерфейсов. Является ли один «более правильным» или это ошибка плохой аннотации в API?

У меня возникла проблема с нулевым анализом при компиляции 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

Это не двусмысленность, а конфликт, поскольку метод не может возвращать что-то одновременно и @NonNull и @Nullable. Eclipse прав (mismatching null constraints), а IntelliJ недостаточно умен, чтобы это обнаружить. IntelliJ полностью упускает из виду эту проблему и обнаруживает только противоречия в аннотациях переопределения и переопределения метода, что совсем другое (но также основано на том факте, что что-то не может быть @NonNull и @Nullable).

howlger 08.04.2024 09:51

Здесь легко похвалить Eclipse, но, как и в случае с IntelliJ, если используется противоположная аннотация @NonNull, ошибка исчезает. Каждая IDE считает, что один из них «правильный», однако он оказывается обратным другому.

Zhro 08.04.2024 17:31

Ваша легкая похвала – это легкий соломенный чувак. Дело в том, что ambiguous() не является двусмысленным, но существуют явно противоречивые ограничения: что-то не может быть @NonNull и @Nullable. ITest implements ITestNullable, ITestNotNull означает «ITest реализует ITestNullable и ITestNotNull», и необходимо гарантировать, что ITest можно использовать как ITestNullable, так и ITestNotNull. Если каждая IDE пропускает один из двух случаев, то оба неверны, что практически одно и то же, а не наоборот. Нулевая аннотация — это ограничение, которое нельзя перезаписать.

howlger 09.04.2024 07:59

@howlger Я обновил свой вопрос. Похоже, это ошибка в Eclipse, которая проявляется только при возникновении этого конкретного конфликта из-за зависимостей, полученных из файлов jar.

Zhro 10.04.2024 09:25

Смотрите мой ответ ниже с примером, доказывающим обратное.

howlger 10.04.2024 15:15

@howlger Ваш ответ не учитывает файлы jar, именно здесь проявляется ошибка.

Zhro 10.04.2024 22:13
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
6
127
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Инверсией, если хотите, Nullable является NonNull, который является частью совершенно не связанного с NotNull процессора аннотаций, поэтому, строго говоря, вы можете использовать оба, и не будет никакого противоречия.

Проблема заключается в обратном поведении между двумя обработчиками аннотаций, а не в самой аннотации. В этом случае все аннотации имеют статус org.jetbrains.annotations, но анализом аннотаций Eclipse они оцениваются по-разному.

Zhro 08.04.2024 00:36

Вам следует настроить обе IDE для запуска одних и тех же обработчиков аннотаций с одинаковыми параметрами (т. е. jetbrains.com/help/idea/annotation-processors-support.html).

Diego Torres Milano 08.04.2024 17:13

Я думаю, вы неправильно понимаете. Аннотации обрабатываются. Нулевой анализ и самоанализ выполняются IDE. Каждая среда обрабатывает это по-своему; в данном случае наоборот.

Zhro 08.04.2024 17:28
Ответ принят как подходящий

В ecj нулевой анализ на основе аннотаций применяется непосредственно к компиляции. При этом основным правилом является соблюдение JLS. В этом примере компилятор ввел область, где JLS 15.12.2.5 требует, чтобы вызов метода выбирал метод из нескольких эквивалентных методов произвольным выбором. После этого нулевой анализ выбора может найти возвращаемый тип @Nullable или @NonNull.

NB: Если не считать нежелательного предупреждения компилятора, двусмысленность безвредна по двум причинам: Из чистой Java p.o.v. Нулевые аннотации не имеют значения ни для компиляции, ни для выполнения. Во-вторых, любой класс, реализующий такой интерфейс, как Player, вынужден (по правилам нулевого анализа) соблюдать оба контракта, и в этом случае побеждает более сильный контракт, а именно возвращающий ненулевое значение.

К счастью, свободу действий, предоставляемую спецификацией («выбрано произвольно»), можно использовать для улучшения анализа нулей в ecj. Благодаря этому исправлению вызывающие Player.getLocation() будут видеть более сильный из обоих контрактов и, таким образом, смогут воспользоваться обещанием, что будет возвращено ненулевое значение.

Спасибо @Zhro и @howlger за сообщение о проблеме и помощь в ее анализе.

Другие вопросы по теме

Похожие вопросы

Автоматизированное тестирование с использованием Selenium, TestNT, Java — Reddit.com — кнопка «Войти» во всплывающем окне
Невозможно имитировать сервер ресурсов JWT Spring для тестирования
Проблема сборки bazel - ключевое слово com.sun.tools.javac или var вызывает сбой
Логика, которую нужно реализовать, чтобы получить максимальное количество очков
Невозможно установить артефакт Maven, хотя он, кажется, указан в репозитории Maven
Задача требует сортировки строки, состоящей из строчных и прописных букв
Почему вызывается пользовательский прослушиватель, когда я не подключаю его к TestClass ни через аннотацию, ни через XML-файл пакета? (ТестНГ)
Почему вызов метода take() Java DelayQueue не блокирует всю структуру данных для всех потоков?
Это фабричный паттерн или паттерн стратегии?
Что происходит с потерянной частью односвязного списка, когда я добавляю цикл в середине?