В этой статье мы проверим проект IntelliJ IDEA Community Edition на наличие ошибок и отправим наши правки разработчикам. Крупный проект, Open Source база и использование статического анализатора при разработке. Сложная задача для PVS-Studio.

О проекте

Каждый Java разработчик знаком с IDEA. У многих разработчиков эта интегрированная среда разработки была первой. Другие, возможно, предпочли эту IDE, попользовавшись другими. Тем не менее нельзя недооценить вклад IntelliJ для Java разработчиков.

IntelliJ IDEA разработана на языке программирования Java. Первая версия выпущена в 2001 году. В 2009 году JetBrains открыли исходный код под лицензией Apache, разрешающей использование и модификацию Community версии продукта. Кодовая база проекта проверяется инструментом Qodana — собственной платформой статического анализа кода.

Open Source контрибьюторы IntelliJ IDEA имеют доступ к открытой базе задач (YouTrack). Такая связка упрощает анализ кодовой базы и поможет нам определить источники и историю найденных нами проблем в коде проекта.

Так как команда разработки большая, а в работе они используют собственный статический анализатор, это серьёзный вызов для PVS-Studio! Запасайтесь печеньками, и мы погружаемся в мир срабатываний анализатора.

Проверяем проект

За пределами реальности

Предлагаю вам найти ошибку в следующем коде:

private static void doTest(@Nullable JBCefClient client) {
  ....
  try {
    browser = new JCEFHtmlPanel(client, "about:blank");
  } catch (RuntimeException ex) {
    fail("Exception occurred: " + ex.getMessage());
    ex.printStackTrace();
  }
  ....
}

В представленном фрагменте как будто проблем нет, но, чтобы их найти, нам нужно знать, что делает метод fail:

public static void fail(String message) {
    if (message == null) {
        throw new AssertionError();
    }
    throw new AssertionError(message);
}

У нас недостижимый код на горизонте! Посмотрите на функцию ex.printStackTrace() в первом фрагменте — мы пытаемся вывести трассировку стека в консоль уже после выброса исключения, из-за которого управление код никогда не получит. В подтверждение этого получаем сообщение:

V6019 Unreachable code detected. It is possible that an error is present. IDEA283718Test.java(55)

Учитывая, что код находится в unit-тестах, вывод stack trace был бы там очень кстати при отладке падения этого теста.

Проанализировав историю коммитов, могу сделать вывод, что ошибка была с момента первого написания в 2021 году.

Предлагаю поднять вывод stack trace до выброса исключений:

private static void doTest(@Nullable JBCefClient client) {
  ....
  try {
    browser = new JCEFHtmlPanel(client, "about:blank");
  } catch (RuntimeException ex) {
    ex.printStackTrace();
    fail("Exception occurred: " + ex.getMessage());
  }
  ....
}

Чудеса копирования

Часто ли вы копируете код вместо того, чтобы написать заново? Но невнимательность подкрадывается незаметно и оставляет свои следы. Следы в виде ошибок. Именно из-за такой практики программирования допускают вот такие опечатки:

private boolean areHomogenous(MouseWheelEvent e1, 
                              MouseWheelEvent e2) {
    if (e1 == null || e2 == null) return false;

    double distance = ....;
    return e1.getComponent() == e2.getComponent() &&
           e1.getID() == e2.getID() &&
           e1.getModifiersEx() == e2.getModifiersEx() &&
           e1.isPopupTrigger() == e1.isPopupTrigger() && // <=
           e1.getScrollType() == e2.getScrollType() &&
           distance < TOLERANCE;
}

В этом методе программист использует неверную переменную и сравнивает значение isPopupTrigger с самим собой. Ничего удивительного, ведь это самый обычный пример ошибки Copy-Paste, которые анализатор, кстати, отлично ловит. Про это у нас есть статьи :)

И вот наше злополучное сообщение:

V6001 There are identical sub-expressions 'e1.isPopupTrigger()' to the left and to the right of the '==' operator. JBCefOsrComponent.java(459)

Вот вам ещё одна ошибка Copy-Paste:

protected int locateUnicodeEscapeSequence(int start, int i) {
    ....
    if (StringUtil.isOctalDigit(c)) {
        if (i + 2 < myBufferEnd && 
            StringUtil.isOctalDigit(myBuffer.charAt(i + 1)) &&
            StringUtil.isOctalDigit(myBuffer.charAt(i + 1))) {
            return i + 3;
        }
    }
    ....
}

Похожая ситуация: при разработке были допущены ошибки при копировании. Видимо, во второй проверке должно быть i + 2, но кто знает, как оно в действительности должно работать?

Адресованное нам сообщение анализатора:

V6001 There are identical sub-expressions 'StringUtil.isOctalDigit(myBuffer.charAt(i + 1))' to the left and to the right of the '&&' operator. JavaStringLiteralLexer.java(64), JavaStringLiteralLexer.java(64)

Делай или не делай

Давайте взглянем на следующий фрагмент кода:

public String buildBeanClass() {
    if (recordsAvailable) {
        out.append("(");
        fields.stream().map(param -> param.getType()
                                          .getCanonicalText(true)
                                          + " " + param.getName());

        StringUtil.join(fields, param -> {
          return ....;
        }, ", ", out);
        out.append("){}");
    } else ....
}

Найдёте проблемную строку в этом коде сами? В качестве подсказки я представляю сообщение, которое нам выдал анализатор:

V6010 The return value of function 'map' is required to be utilized. ParameterObjectBuilder.java(103)

Объясняем. Мы создаём stream из коллекции fields и:

  1. Не применяем терминирующей операции, используем только промежуточную операцию map (без терминирующей операции результат не будет получен);

  2. Не используем результаты операции.

Буквально мы можем удалить эту строчку, и никто про неё не вспомнит.

Волшебство целочисленного деления

Представляю вам деление:

public class RectanglePainter2DTest extends AbstractPainter2DTest {
    private static final int RECT_SIZE = 10;
    private static final double ARC_SIZE = RECT_SIZE / 3;
    ...
}

Но деление-то не простое, а целочисленное, а значит мы вернём целое значение без знаков после запятой. Но переменная-то double. И во всех местах, где она используется, она представлена в виде double переменной.

Посмотрим сообщение анализатора:

V6094 The expression was implicitly cast from 'int' type to 'double' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. RectanglePainter2DTest.java(24)

Могу ли я уверять, что срабатывание точно положительное? Думаю, что нет. Но мы такое ловим, и анализатор правильно поругался на потерю значений после запятой. Учитывая, что код используется в unit-тесте, — это, с одной стороны, не страшно. С другой — неточный набор данных, нот кул. В любом случае предложенное исправление будет таким:

private static final double ARC_SIZE = RECT_SIZE / 3f;

Рефакторинг, который привёл к избытку

Вы когда-нибудь думали, что иногда мы тратим время на что-то неоправданное? Так вот, именно здесь этим мы и занимаемся:

protected List<....> getEncodeReplacements(CharSequence input) {
  for (int i = 0; i < input.length(); ++i) {
    if (input.charAt(i) == '\n') {
      final String replacement;
      if (i + 1 >= input.length() ||
        YAMLGrammarCharUtil.isSpaceLike(input.charAt(i + 1)) ||
        input.charAt(i + 1) == '\n' || 
        currentLineIsIndented) 
      {
        replacement = "\n" + indentString;
      }
      else 
      {
        replacement = "\n" + indentString;
      }
      ....
      continue;
    }
    ....
  }
}

Посмотрим сообщение:

V6004 The 'then' statement is equivalent to the 'else' statement. YAMLScalarTextImpl.java(116), YAMLScalarTextImpl.java(119)

Вне зависимости от условия выполнится одно и то же тело метода. Что больше всего пугает — из-за этой проверки код становится очень нечитабельным.

Просмотрев историю коммитов, могу сказать, что когда-то блок ветвления выглядел вот так:

if (i + 1 >= input.length() ||
    YAMLGrammarCharUtil.isSpaceLike(input.charAt(i + 1)) ||
    input.charAt(i + 1) == '\n' || 
    currentLineIsIndented) {
    replacement = "\n" + indentString;
}
else {
    replacement = "\n\n" + indentString;
}

В какой-то момент один символ убрали со словами Yaml: 'YamlScalarTextEvaluator' extracted to implement old 'getTextValue' behavior. А мы спорить не будем, исправим код, уберём всю проверку, оставим только нужное:

for (int i = 0; i < input.length(); ++i) {
    if (input.charAt(i) == '\n') {
        final String replacement = "\n" + indentString;
        ....
        continue;
    }
    ....
}

Проделки одной константы

Привожу вам сокращённый фрагмент кода:

@Override
protected void customizeComponent(JList list, E value, boolean isSelected) {
    ....
    boolean nextStepButtonSelected = false; // <=
    ....
    myMnemonicLabel.setForeground(isSelected && 
                                  isSelectable && 
                                  !nextStepButtonSelected 
                                  ? getSelectionForeground() 
                                  : foreground);
    ....
    myShortcutLabel.setForeground(
      isSelected && 
      isSelectable && 
      !nextStepButtonSelected
      ? UIManager.getColor("MenuItem.acceleratorSelectionForeground")
      : UIManager.getColor("MenuItem.acceleratorForeground"));
    ....
    boolean selected = isSelected && isSelectable && !nextStepButtonSelected;

    setForegroundSelected(myValueLabel, selected);
    ....
}

Как вы думаете, что здесь не так? Переменная nextStepButtonSelected всегда false.

"Но на ней завязана куча поведения", — скажете вы. "Неважно", — отвечу вам.

V6007 Expression '!nextStepButtonSelected' is always true. PopupListElementRenderer.java(330)

V6007 Expression '!nextStepButtonSelected' is always true. PopupListElementRenderer.java(384)

V6007 Expression '!nextStepButtonSelected' is always true. PopupListElementRenderer.java(373)

Сразу три сообщения — нехорошо. Проанализировав код, могу сказать, что разработчики хотели отказаться от поведения, но убирать саму переменную не стали. У нас получилась константа false, которая привела к плохой читабельности и возможным проблемам при изменении кода в будущем.

Заключение

На этом, я думаю, нам стоит остановиться. Мы сделали pull request для разработчиков IDEA, и я поставленные задачи выполнил. Действительно, я рад помочь разработчикам своей любимой IDE.

Отличный результат проверки действующего проекта, который, к тому же, уже использует стат. анализ.

А вы хотите попробовать статический анализ? Вы всегда можете внедрить его в свой проект с помощью PVS-Studio, скачав по ссылке.

Комментарии (6)


  1. GolyshkinAlexey
    14.12.2023 11:38

    Спасибо за статью


  1. ris58h
    14.12.2023 11:38

    Сама IDEA ни одну из этих ошибок не обнаружила?


    1. TheLivan Автор
      14.12.2023 11:38

      Ну к примеру про ошибки формата Copy-Paste не подсказывает, ну это в принципе было логично

      Первое срабатывание Copy-Paste
      Первое срабатывание Copy-Paste
      Второе срабатывание Copy-Paste
      Второе срабатывание Copy-Paste

      Зато одинаковые блоки предлагает сократить:

      Зато обычные блоки предлагает сократить
      Зато обычные блоки предлагает сократить

      Стат. анализ думаю подсказки в IDE не заменят. Цели и задачи то разные.


      1. ValeryIvanov
        14.12.2023 11:38

        Ну к примеру про ошибки формата Copy-Paste не подсказывает, ну это в принципе было логично

        Не совсем. По какой-то странной причине, IDEA не обнаруживает подобные ошибки при сравнении через ==, но ругается при использовании Objects.equals(obj, obj) или `obj.equals(obj)


    1. Andrey2008
      14.12.2023 11:38

      Даже если обнаружила, это ничего не меняет. Ведь в коде то ошибки есть, а значит среда разработки не заменяет классические анализаторы кода, такие как PVS-Studio.

      Они по разному используется. Задача среды - по возможности подсветить ошибки (пойдя на компромисс между глубиной анализа и скоростью).

      Задача статического анализатора – провести глубокий анализ и предоставить гибкую многоуровневую систему контроля качества кода. Если разработчик не заметит/проигнорирует ошибку на этапе написания или закладке кода, то предупреждения будут разосланы, например после ночной проверки. В том числе, письмо с багами придёт тимлиду и он придёт к автору кода с наставлениями :) Это один из сценариев, возможны и другие.


  1. Antharas
    14.12.2023 11:38

    «Больше инспекций богу инспекций» (с)

    Скоро совсем перестанет анализировать код, вернемся к временам notepad.