Два года назад я вызвался постоять на стенде нашей компании JetBrains на последней конференции JBreak в Новосибирске. Перед конференцией мне спустили сверху вот такие карточки:



И сказали, мол, ну раздай каким-нибудь людям на конференции на своё усмотрение. Я запаниковал. Как же я буду людей-то выбирать?


Тогда я довольно плотно работал с анализом потока данных в статическом анализаторе IntelliJ IDEA для Java. Во-первых, я обкатывал новые фичи, проверяя код самой IDE. Во-вторых, разгребал входящие баг-репорты. Иногда IDEA находила удивительные проблемы, и мне приходилось долго разбираться, чтобы вообще понять, правильное ли предупреждение она выдаёт или это баг.


Пока шёл первый доклад конференции, я решил из этого материала быстренько сварганить игру. Набрал пачку свежих примеров из головы (в основном из реального кода). В каждом случае стояла задача объяснить предупреждение, которое выдаёт IDEA, или обосновать, что предупреждение ложное. За правильные ответы я и раздавал трёхмесячные лицензии на все наши десктопные продукты.


Теперь я предлагаю те же самые задачки решить вам. Многие простые, на внимательность. Но есть такие, где надо хорошенько подумать.


1 Обход списка



Отгадка

Предупреждение верное. Мы не знаем ни длину входного списка args, ни длину массива parameters. Однако сколько бы ни было параметров на первой итерации цикла i == 0, а длина списка не может быть меньше нуля по контракту. Это означает, что до второй итерации дело никогда не дойдёт, потому что исключение вылетит уже на первой.


2 Наилучшая точка



Отгадка

Предупреждение верное. Массив variants определён прямо здесь и имеет четыре элемента. Значит, мы точно зайдём в цикл. На первой итерации if (best == null || distance > d) совершенно точно истинно, потому что в начале best == null. Значит, best = variant будет точно выполнено. А variant, конечно, никогда не null. Даже если бы мы не видели инициализаторов массива, мы можем это сказать потому что был вызван метод variant.distance.


3 Свойства драйверов



Отгадка

Предупреждение ошибочное. В переменной value действительно может быть null, но в этом случае массив result будет пустым, и мы в цикл никак не зайдём. IDEA 2018.1 не понимала таких тонкостей, но в скором времени мы её научили. Теперь этого предупреждения нет.


4 Нулл или не нулл?



Отгадка

Этот код прислал пользователь в наш баг-трекер, сказав, что IDEA выдаёт ложное предупреждение. На самом деле предупреждение верное. Входной массив имеет заведомо ненулевую длину, операция map не меняет число элементов стрима (возможно, автор хотел написать filter), а значит, операция findFirst точно что-то найдёт и ветка orElse(null) никогда не выполнится.


5 Инициализация поля



Может быть все дочерние классы по контракту устанавливают поле name, и предупреждение ложно?


Отгадка

Предупреждение в целом верное. Только стоит писать не "may produce", а "will produce" (в свежих версиях это исправлено). У нас нет шанса инициализировать поле name, какая бы ни была реализация метода init(). Дело в том что объект this не утекает из конструктора. Метод init() вызывается, но на родительском объекте, который передан параметром. Из него мы никак не сможем доступиться до объекта, который сейчас конструируется. Да, init() может устанавливать name родительского объекта, но name текущего ему не доступен, а значит, мы можем быть уверены, что после вызова init() там всё ещё null.


6 Генерация имён классов



Отгадка

Предупреждение верное. Я разбирал этот случай в отдельной статье "Статический анализ > уязвимость > профит"


7 Индикаторы



Отгадка

Предупреждение ложное. Видно, что подсвеченный метод вызовется только при indicator == 2, а это возможно только если и riseBinding, и sinkBinding не равный null. С тех пор, конечно, IDEA поумнела и нормально разбирается в таком коде, не выдавая предупреждения.


8 Лямбда с побочным эффектом



Отгадка

Предупреждение верное. Мы видим лямбду, в которой exception[0] может перезаписаться заведомо ненулевым исключением. Проблема однако в том, что до условного оператора лямбда ни разу не могла быть выполнена. Она используется после условия. Соответственно, exception[0] не мог быть перезаписан. Вероятно, условие и последующий вызов следует переставить местами.


9 Груви-туплы



Отгадка

Предупреждение верное. Тут всё просто: вместо i < initializers.length было написано initializers.length < i. Соответственно индекс массива всегда больше его длины. Учитывая, что этот код существовал давно, скорее всего неправильное условие никогда не выполнялось и до исключения дело не доходило. Интересно, что return в котором подсвечена ошибка — единственный полезный выхлоп всего условия if (parent instanceof GrTuple). То есть десять строчек этого метода никогда не делали никакой полезной работы.


10 Префиксы



Отгадка

Тоже несложно. filePrefix и linePrefix могут быть переприсвоены только одновременно. Им присваиваются элементы массивов filePrefixes и linePrefixes соответственно, в которых нет нуллов. То есть если filePrefix оказался не нуллом, то и в linePrefix нулла быть не может. Предупреждение верное, хотя в целом это не ошибка, просто перестраховочное условие.



Надеюсь, задачки вам понравились. К чему я всё это вспомнил через два года? Всё потому что 29 февраля в Новосибирске пройдёт новая Java-конференция SnowOne. JetBrains также будет спонсировать эту конференцию, и я сейчас вовсю готовлю новую порцию задачек. Призы будут такие же — трёхмесячная лицензия на все продукты JetBrains. Приходите поиграть!