Два года назад я вызвался постоять на стенде нашей компании 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. Приходите поиграть!
Автор: Тагир Валеев