
Статический анализатор PVS-Studio известен в мире C, C++ и C# как инструмент для выявления ошибок и потенциальных уязвимостей. Однако у нас мало клиентов из финансового сектора, так как выяснилось, что сейчас там востребованы Java и IBM RPG(!). Нам же всегда хотелось стать ближе к миру Enterprise, поэтому, после некоторых раздумий, мы приняли решение заняться созданием Java анализатора.
Введение
Конечно, были и опасения. Легко занять рынок анализаторов в IBM RPG. Я вообще не уверен, что существуют достойные инструменты для статического анализа этого языка. В мире Java дела обстоят совсем иначе. Уже есть линейка инструментов для статического анализа, и чтобы вырваться вперед, нужно создать действительно мощный и крутой анализатор.
Тем не менее, в нашей компании был опыт использования нескольких инструментов для статического анализа Java, и мы уверены, что многие вещи мы сможем сделать лучше.
Кроме того, у нас была идея, как задействовать всю мощь нашего С++ анализатора в Java анализаторе. Но обо всём по порядку.
Дерево

В первую очередь нужно было определиться с тем, каким образом мы будем получать синтаксическое дерево и семантическую модель.
Синтаксическое дерево является базовым элементом, вокруг которого строится анализатор. При выполнении проверок анализатор перемещается по синтаксическому дереву и исследует его отдельные узлы. Без такого дерева производить серьезный статический анализ практически невозможно. Например, поиск ошибок с помощью регулярных выражений является бесперспективным.
При этом стоит отметить, что одного лишь синтаксического дерева недостаточно. Анализатору необходима и семантическая информация. Например, нам нужно знать типы всех элементов дерева, иметь возможность перейти к объявлению переменной и т.п.
Мы рассмотрели несколько вариантов получения синтаксического дерева и семантической модели:
- ANTLR (с грамматикой для Java)
- JavaParser и JavaSymbolSolver
- Eclipse's ASTParser из Eclipse JDT
- Spoon
От идеи использования ANTLR мы отказались практически сразу, так как это неоправданно усложнило бы разработку анализатора (семантический анализ пришлось бы реализовывать своими силами). В конечном итоге решили остановиться на библиотеке Spoon:
- Является не просто парсером, а целой экосистемой — предоставляет не только дерево разбора, но и возможности для семантического анализа, например, позволяет получить информацию о типах переменных, перейти к объявлению переменной, получить информацию о родительском классе и так далее.
- Основывается на Eclipse JDT и умеет компилировать код.
- Поддерживает последнюю версию Java и постоянно обновляется.
- Неплохая документация и понятный API.
Вот пример метамодели, которую предоставляет Spoon, и с которой мы работаем при создании диагностических правил:

Эта метамодель соответствует следующему коду:
class TestClass { void test(int a, int b) { int x = (a + b) * 4; System.out.println(x); } }
Одной из приятных особенностей Spoon является то, что он упрощает синтаксическое дерево (удаляя и добавляя узлы) для того, чтобы с ним было проще работать. При этом гарантируется семантическая эквивалентность упрощенной метамодели исходной.
Для нас это означает, например, что не нужно больше заботиться о пропуске лишних скобок при обходе дерева. Помимо этого, каждое выражение помещается в блок, раскрываются импорты, производятся еще некоторые похожие упрощения.
Например, такой код:
for (int i = ((0)); (i < 10); i++) if (cond) return (((42)));
будет представлен следующим образом:
for (int i = 0; i < 10; i++) { if (cond) { return 42; } }
На основе синтаксического дерева производится так называемый pattern-based анализ. Это поиск ошибок в исходном коде программы по известным шаблонам кода с ошибкой. В простейшем случае анализатор ищет в дереве места, похожие на ошибку, согласно правилам, описанным в соответствующей диагностике. Количество подобных шаблонов велико и их сложность может сильно варьироваться.
Простейшим примером ошибки, обнаруживаемой с помощью pattern-based анализа, может служить следующий код из проекта jMonkeyEngine:
if (p.isConnected()) { log.log(Level.FINE, "Connection closed:{0}.", p); } else { log.log(Level.FINE, "Connection closed:{0}.", p); }
Блоки then и else оператора if полностью совпадают, скорее всего, имеется логическая ошибка.
Вот еще один подобный пример из проекта Hive:
if (obj instanceof Number) { // widening conversion return ((Number) obj).doubleValue(); } else if (obj instanceof HiveDecimal) { // <= return ((HiveDecimal) obj).doubleValue(); } else if (obj instanceof String) { return Double.valueOf(obj.toString()); } else if (obj instanceof Timestamp) { return new TimestampWritable((Timestamp)obj).getDouble(); } else if (obj instanceof HiveDecimal) { // <= return ((HiveDecimal) obj).doubleValue(); } else if (obj instanceof BigDecimal) { return ((BigDecimal) obj).doubleValue(); }
В данном коде находятся два одинаковых условия в последовательности вида if (....) else if (....) else if (....). Стоит проверить этот участок кода на наличие логической ошибки, либо убрать дублирующий код.
Data-flow analysis
Помимо синтаксического дерева и семантической модели, анализатору необходим механизм анализа потока данных.
Анализ потока данных позволяет вычислять допустимые значения переменных и выражений в каждой точке программы и, благодаря этому, находить ошибки. Мы называем эти допустимые значения виртуальными значениями.
Виртуальные значения создаются для переменных, полей классов, параметров методов и прочего при первом упоминании. Если это присваивание, механизм Data Flow вычисляет виртуальное значение путем анализа выражения стоящего справа, в противном случае в качестве виртуального значения берется весь допустимый диапазон значений для данного типа переменной. Например:
void func(byte x) // x: [-128..127] { int y = 5; // y: [5] ... }
При каждом изменении значения переменной механизм Data Flow пересчитывает виртуальное значение. Например:
void func() { int x = 5; // x: [5] x += 7; // x: [12] ... }
Механизм Data Flow также выполняет обработку управляющих операторов:
void func(int x) // x: [-2147483648..2147483647] { if (x > 3) { // x: [4..2147483647] if (x < 10) { // x: [4..9] } } else { // x: [-2147483648..3] } ... }
В данном примере при входе в функцию о диапазоне значений переменной x нет никакой информации, поэтому он устанавливается в соответствии с типом переменной (от -2147483648 до 2147483647). Затем первый условный блок накладывает ограничение x > 3, и диапазоны объединяются. В результате в then блоке диапазон значений для x: от 4 до 2147483647, а в else блоке от -2147483648 д�� 3. Аналогичным образом обрабатывается и второе условие x < 10.
Кроме того, необходимо иметь возможность производить и чисто символьные вычисления. Простейший пример:
void f1(int a, int b, int c) { a = c; b = c; if (a == b) // <= always true .... }
Здесь переменной а присваивается значение c, переменной b также присваивается значение c, после чего а и b сравниваются. В этом случае, чтобы найти ошибку, достаточно просто запомнить кусок дерева, соответствующий правой части.
Вот чуть более сложный пример с символьными вычислениями:
void f2(int a, int b, int c) { if (a < b) { if (b < c) { if (c < a) // <= always false .... } } }
В таких случаях уже приходится решать систему неравенств в символьном виде.
Механизм Data Flow помогает анализатору находить ошибки, которые с помощью pattern-based анализа найти весьма затруднительно.
К таким ошибкам относятся:
- Переполнения;
- Выход за границу массива;
- Доступ по нулевой или потенциально нулевой ссылке;
- Бессмысленные условия (always true/false);
- Утечки памяти и ресурсов;
- Деление на 0;
- И некоторые другие.
Data Flow анализ особенно важен при поиске уязвимостей. Например, в случае, если некая программа получает ввод от пользователя, есть вероятность, что ввод будет использован для того, чтобы вызвать отказ в обслуживании, или для получения контроля над системой. Примерами могут служить ошибки, приводящие к переполнениям буфера при некоторых входных данных, или, например, SQL-инъекции. В обоих случаях для того, чтобы статический анализатор мог выявлять подобные ошибки и уязвимости, необходимо отслеживать поток данных и возможные значения переменных.
Должен сказать, что механизм анализа потока данных — это сложный и обширный механизм, а в этой статье я коснулся лишь самых основ.
Рассмотрим несколько примеров ошибок, которые можно обнаружить с помощью механизма Data Flow.
Проект Hive:
public static boolean equal(byte[] arg1, final int start1, final int len1, byte[] arg2, final int start2, final int len2) { if (len1 != len2) { // <= return false; } if (len1 == 0) { return true; } .... if (len1 == len2) { // <= .... } }
Условие len1 == len2 выполняется всегда, поскольку противоположная проверка уже была выполнена выше.
Еще один пример из этого же проекта:
if (instances != null) { // <= Set<String> oldKeys = new HashSet<>(instances.keySet()); if (oldKeys.removeAll(latestKeys)) { .... } this.instances.keySet().removeAll(oldKeys); this.instances.putAll(freshInstances); } else { this.instances.putAll(freshInstances); // <= }
Здесь в блоке else гарантированно происходит разыменование нулевого указателя. Примечание: здесь instances — это то же самое, что и this.instances.
Пример из проекта JMonkeyEngine:
public static int convertNewtKey(short key) { .... if (key >= 0x10000) { return key - 0x10000; } return 0; }
Здесь переменная key сравнивается с числом 65536, однако, она имеет тип short, а максимально возможное значение для short — это 32767. Соответственно, условие никогда не выполняется.
Пример из проекта Jenkins:
public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; }
В данном коде ввели переменную cnt, чтобы ограничить количество проходов пятью, но забыли ее инкрементировать, в результате чего проверка бесполезна.
Механизм аннотаций
Кроме того, анализатору нужен механизм аннотаций. Аннотации — это система разметки, которая предоставляет анализатору дополнительную информацию об используемых методах и классах, помимо той, что может быть получена путем анализа их сигнатуры. Разметка производится вручную, это долгий и трудоемкий процесс, так как для достижения наилучших результатов необходимо проаннотировать большое количество стандартных классов и методов языка Java. Также имеет смысл выполнить аннотирование популярных библиотек. В целом аннотации можно рассматривать как базу знаний анализатора о контрактах стандартных методов и классов.
Вот небольшой пример ошибки, которую можно обнаружить с помощью аннотаций:
int test(int a, int b) { ... return Math.max(a, a); }
В данном примере из-за опечатки в качестве второго аргумента метода Math.max была передана та же переменная, что и в качестве первого аргумента. Такое выражение является бессмысленным и подозрительным.
Зная о том, что аргументы метода Math.max всегда должны быть различны, статический анализатор сможет выдать предупреждение на подобный код.
Забегая вперед, приведу несколько примеров нашей разметки встроенных классов и методов (код на C++):
Class("java.lang.Math") - Function("abs", Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Returns(Arg1, [](const Int &v) { return v.Abs(); }) - Function("max", Type::Int32, Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(Arg1, Arg2) .Returns(Arg1, Arg2, [](const Int &v1, const Int &v2) { return v1.Max(v2); }) Class("java.lang.String", TypeClassification::String) - Function("split", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotNull(Arg1)) .Returns(Ptr(NotNullPointer)) Class("java.lang.Object") - Function("equals", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(This, Arg1)) Class("java.lang.System") - Function("exit", Type::Int32) .Set(FunctionClassification::NoReturn)
Пояснения:
- Class – аннотируемый класс;
- Function – метод аннотируемого класса;
- Pure – аннотация, показывающая, что метод является чистым, т.е. детерминированным и не имеющим побочных эффектов;
- Set – установка произвольного флага для метода.
- FunctionClassification::NoDiscard – флаг, означающий, что возвращаемое значение метода обязательно должно быть использовано;
- FunctionClassification::NoReturn – флаг, означающий, что метод не возвращает управление;
- Arg1, Arg2, ..., ArgN – аргументы метода;
- Returns – возвращаемое значение метода;
- Requires – контракт на метод.
Стоит отметить, что помимо ручной разметки существует еще один подход к аннотированию – автоматический вывод контрактов на основе байт-кода. Понятно, что такой подход позволяет вывести только определенные виды контрактов, но зато он дает возможность получать дополнительную информацию вообще из всех зависимостей, а не только из тех, что были проаннотированы вручную.
К слову, уже существует инструмент, который умеет выводить контракты вроде @Nullable, NotNull на основе байт-кода – FABA. Насколько я понимаю, производная от FABA используется в IntelliJ IDEA.
Сейчас мы тоже рассматриваем возможность добавления анализа байт-кода для получения контрактов для всех методов, так как эти контракты могли бы хорошо дополнить наши ручные аннотации.
Диагностические правила при работе часто обращаются к аннотациям. Помимо диагностик, аннотации использует механизм Data Flow. Например, используя аннотацию метода java.lang.Math.abs, он может точно вычислять значение для модуля числа. При этом писать какой-либо дополнительный код не приходится – только правильно разметить метод.
Рассмотрим пример ошибки из проекта Hibernate, которую можно обнаружить благодаря аннотации:
public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } }
В данном коде метод equals() сравнивает объект purchaseSequence с самим собой. Наверняка это опечатка и справа должно быть that.purchaseSequence, а не purchaseSequence.
Как доктор Франкенштейн собирал из частей анализатор

Поскольку сами по себе механизмы Data Flow и аннотаций не сильно привязаны к определенному языку, было принято решение переиспользовать эти механизмы из нашего C++ анализатора. Это позволило нам в короткие сроки получить всю мощь ядра C++ анализатора в нашем Java анализаторе. Кроме того, на это решение также повлияло и то, что эти механизмы были написаны на современном C++ с кучей метапрограммирования и шаблонной магии, а, соответственно, не очень хорошо подходят для переноса на другой язык.
Для того, чтобы связать Java часть с ядром на C++, мы решили использовать SWIG (Simplified Wrapper and Interface Generator) – средство для автоматической генерации врапперов и интерфейсов для связывания программ на C и C++ с программами, написанными на других языках. Для Java SWIG генерирует код на JNI (Java Native Interface).
SWIG отлично подходит для случаев, когда имеется уже большой объем C++ кода, который необходимо интегрировать в Java проект.
Приведу минимальный пример работы со SWIG. Предположим, у нас есть C++ класс, который мы хотим использовать в Java проекте:
CoolClass.h
class CoolClass { public: int val; CoolClass(int val); void printMe(); };
CoolClass.cpp
#include <iostream> #include "CoolClass.h" CoolClass::CoolClass(int v) : val(v) {} void CoolClass::printMe() { std::cout << "val: " << val << '\n'; }
Сперва нужно создать интерфейсный файл SWIG с описанием всех экспортируемых функций и классов. Также в этом файле при необходимости производятся дополнительные настройки.
Example.i
%module MyModule %{ #include "CoolClass.h" %} %include "CoolClass.h"
После этого можно запускать SWIG:
$ swig -c++ -java Example.i
Он сгенерирует следующие файлы:
- CoolClass.java – класс, с которым мы будем непосредственно работать в Java проекте;
- MyModule.java – класс м��дуля, в который помещаются все свободные функции и переменные;
- MyModuleJNI.java – Java врапперы;
- Example_wrap.cxx – С++ врапперы.
Теперь нужно просто добавить получившиеся .java файлы в Java проект и .cxx файл в C++ проект.
Наконец, нужно скомпилировать C++ проект в виде динамической библиотеки и подгрузить ее в Java проект с помощью System.loadLibary():
App.java
class App { static { System.loadLibary("example"); } public static void main(String[] args) { CoolClass obj = new CoolClass(42); obj.printMe(); } }
Схематически это можно представить следующим образом:

Конечно, в реальном проекте все не настолько просто и приходится приложить чуть больше усилий:
- Для того, чтобы использовать шаблонные классы и методы из C++, их нужно инстанцировать для всех принимаемых шаблонных параметров с помощью директивы %template;
- В некоторых случаях может понадобиться перехватывать исключения, которые выбрасываются из C++ части в Java части. По умолчанию SWIG не перехватывает исключения из C++ (возникает segfault), однако, имеется возможность это сделать, используя директиву %exception;
- SWIG позволяет расширять плюсовый код на стороне Java, используя директиву %extend. Мы, например, в нашем проекте добавляем к виртуальным значениям метод toString(), чтобы можно было их просмотреть в отладчике Java;
- Для того, чтобы эмулировать RAII поведение из C++, во всех интересующих классах реализуется интерфейс AutoClosable;
- Механизм директоров позволяет использовать кросс-языковый полиморфизм;
- Для типов, которые аллоцируются только внутри C++ (на своём пуле памяти), убираются конструкторы и финализаторы, чтобы повысить производительность. Сборщик мусора будет игнорировать эти типы.
Подробнее обо всех этих механизмах можно почитать в документации SWIG.
Наш анализатор собирается при помощи gradle, который зовет CMake, который, в свою очередь, зовет SWIG и собирает C++ часть. Для программистов это происходит практически незаметно, так что никаких особых неудобств при разработке мы не испытываем.
Ядро нашего C++ анализатора собирается под Windows, Linux, macOS, так что Java анализатор также работает в этих ОС.
Что из себя представляет диагностическое правило?
Сами диагностики и код для анализа мы пишем на Java. Это обусловлено тесным взаимодействием со Spoon. Каждое диагностическое правило является визитором, у которого перегружаются методы, в которых обходятся интересующие нас элементы:

Например, так выглядит каркас диагностики V6004:
class V6004 extends PvsStudioRule { .... @Override public void visitCtIf(CtIf ifElement) { // if ifElement.thenStatement statement is equivalent to // ifElement.elseStatement statement => add warning V6004 } }
Плагины
Для максимально простой интеграции статического анализатора в проект мы разработали плагины для сборочных систем Maven и Gradle. Пользователю остается только добавить наш плагин к проекту.
Для Gradle:
.... apply plugin: com.pvsstudio.PvsStudioGradlePlugin pvsstudio { outputFile = 'path/to/output.json' .... }
Для Maven:
.... <plugin> <groupId>com.pvsstudio</groupId> <artifactId>pvsstudio-maven-plugin</artifactId> <version>0.1</version> <configuration> <analyzer> <outputFile>path/to/output.json</outputFile> .... </analyzer> </configuration> </plugin>
После этого плагин самостоятельно получит структуру проекта и запустит анализ.
Помимо этого, мы разработали прототип плагина для IntelliJ IDEA.

Также этот плагин работает и в Android Studio.
Плагин для Eclipse сейчас находится в стадии разработки.
Инкрементальный анализ
Мы предусмотрели режим инкрементального анализа, который позволяет проверять только измененные файлы и тем самым значительно сокращает время, необходимое для анализа кода. Благодаря этому, разработчики смогут запускать анализ так часто, как это необходимо.
Инкрементальный анализ включает в себя несколько этапов:
- Кеширование метамодели Spoon;
- Перестроение изменившейся части метамодели;
- Анализ изменившихся файлов.
Наша система тестирования
Для тестирования Java анализатора на реальных проектах мы написали специальный инструментарий, позволяющий работать с базой открытых проектов. Он был написан на коленке^W Python + Tkinter и является кроссплатформенным.
Он работает следующим образом:
- Тестируемый проект определенной версии выкачивается с репозитория на GitHub;
- Выполняется сборка проекта;
- В pom.xml или build.gradle добавляется наш плагин (при помощи git apply);
- Выполняется запуск статического анализатора при помощи плагина;
- Получившийся отчет сравнивается с эталоном для этого проекта.
Такой подход гарантирует, что хорошие срабатывания не пропадут в результате изменения кода анализатора. Ниже показан интерфейс нашей утилиты для тестирования.

Красным отмечаются те проекты, в отчетах которых есть какие-либо различия с эталоном. Кнопка Approve позволяет сохранить текущую версию отчета в качестве эталона.
Примеры ошибок
По традиции приведу по несколько ошибок из разных открытых проектов, которые нашел наш Java анализатор. В будущем планируется написание статей с более детальным отчетом по каждому проекту.
Проект Hibernate
Предупреждение PVS-Studio: V6009 Function 'equals' receives odd arguments. Inspect arguments: this, 1. PurchaseRecord.java 57
public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } }
В данном коде метод equals() сравнивает объект purchaseSequence с самим собой. Скорее всего, это опечатка и справа должно быть that.purchaseSequence, а не purchaseSequence.
Предупреждение PVS-Studio: V6009 Function 'equals' receives odd arguments. Inspect arguments: this, 1. ListHashcodeChangeTest.java 232
public void removeBook(String title) { for( Iterator<Book> it = books.iterator(); it.hasNext(); ) { Book book = it.next(); if ( title.equals( title ) ) { it.remove(); } } }
Срабатывание, аналогичное предыдущему – справа должно быть book.title, а не title.
Проект Hive
Предупреждение PVS-Studio: V6007 Expression 'colOrScalar1.equals(«Column»)' is always false. GenVectorCode.java 2768
Предупреждение PVS-Studio: V6007 Expression 'colOrScalar1.equals(«Scalar»)' is always false. GenVectorCode.java 2774
Предупреждение PVS-Studio: V6007 Expression 'colOrScalar1.equals(«Column»)' is always false. GenVectorCode.java 2785
String colOrScalar1 = tdesc[4]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column")) { .... } else if (colOrScalar1.equals("Col") && colOrScalar1.equals("Scalar")) { .... } else if (colOrScalar1.equals("Scalar") && colOrScalar1.equals("Column")) { .... }
Здесь явно перепутали операторы и вместо '||' использовали '&&'.
Проект JavaParser
Предупреждение PVS-Studio: V6001 There are identical sub-expressions 'tokenRange.getBegin().getRange().isPresent()' to the left and to the right of the '&&' operator. Node.java 213
public Node setTokenRange(TokenRange tokenRange) { this.tokenRange = tokenRange; if (tokenRange == null || !(tokenRange.getBegin().getRange().isPresent() && tokenRange.getBegin().getRange().isPresent())) { range = null; } else { range = new Range( tokenRange.getBegin().getRange().get().begin, tokenRange.getEnd().getRange().get().end); } return this; }
Анализатор обнаружил, что слева и справа от оператора && стоят одинаковые выражения (при этом все методы в цепочке вызовов являются чистыми). Скорее всего, во втором случае необходимо использовать tokenRange.getEnd(), а не tokenRange.getBegin().
Предупреждение PVS-Studio: V6016 Suspicious access to element of 'typeDeclaration.getTypeParameters()' object by a constant index inside a loop. ResolvedReferenceType.java 265
if (!isRawType()) { for (int i=0; i<typeDeclaration.getTypeParams().size(); i++) { typeParametersMap.add( new Pair<>(typeDeclaration.getTypeParams().get(0), typeParametersValues().get(i))); } }
Анализатор обнаружил подозрительный доступ к элементу коллекции по константному индексу внутри цикла. Возможно, в данном коде присутствует ошибка.
Проект Jenkins
Предупреждение PVS-Studio: V6007 Expression 'cnt < 5' is always true. AbstractProject.java 557
public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; }
В данном коде ввели переменную cnt, чтобы ограничить количество проходов пятью, но забыли ее инкрементировать, в результате чего проверка бесполезна.
Проект Spark
Предупреждение PVS-Studio: V6007 Expression 'sparkApplications != null' is always true. SparkFilter.java 127
if (StringUtils.isNotBlank(applications)) { final String[] sparkApplications = applications.split(","); if (sparkApplications != null && sparkApplications.length > 0) { ... } }
Проверка на null результата, возвращаемого методом split, бессмысленна, так как этот метод всегда возвращает коллекцию и никогда не возвращает null.
Проект Spoon
Предупреждение PVS-Studio: V6001 There are identical sub-expressions '!m.getSimpleName().startsWith(«set»)' to the left and to the right of the '&&' operator. SpoonTestHelpers.java 108
if (!m.getSimpleName().startsWith("set") && !m.getSimpleName().startsWith("set")) { continue; }
В данном коде слева и справа от оператора && стоят одинаковые выражения (при этом все методы в цепочке вызовов являются чистыми). Скорее всего, в коде присутствует логическая ошибка.
Предупреждение PVS-Studio: V6007 Expression 'idxOfScopeBoundTypeParam >= 0' is always true. MethodTypingContext.java 243
private boolean isSameMethodFormalTypeParameter(....) { .... int idxOfScopeBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) { // <= int idxOfSuperBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) { // <= return idxOfScopeBoundTypeParam == idxOfSuperBoundTypeParam; } } .... }
Здесь опечатались во втором условии и вместо idxOfSuperBoundTypeParam написали idxOfScopeBoundTypeParam.
Проект Spring Security
Предупреждение PVS-Studio: V6001 There are identical sub-expressions to the left and to the right of the '||' operator. Check lines: 38, 39. AnyRequestMatcher.java 38
@Override @SuppressWarnings("deprecation") public boolean equals(Object obj) { return obj instanceof AnyRequestMatcher || obj instanceof security.web.util.matcher.AnyRequestMatcher; }
Срабатывание аналогично предыдущему – здесь имя одного и того же класса записано разными способами.
Предупреждение PVS-Studio: V6006 The object was created but it is not being used. The 'throw' keyword could be missing. DigestAuthenticationFilter.java 434
if (!expectedNonceSignature.equals(nonceTokens[1])) { new BadCredentialsException( DigestAuthenticationFilter.this.messages .getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[] { nonceAsPlainText }, "Nonce token compromised {0}")); }
В данном коде забыли добавить throw перед исключением. В результате объект исключения BadCredentialsException создается, но никак не используется, т.е. исключение не выбрасывается.
Предупреждение PVS-Studio: V6030 The method located to the right of the '|' operators will be called regardless of the value of the left operand. Perhaps, it is better to use '||'. RedirectUrlBuilder.java 38
public void setScheme(String scheme) { if (!("http".equals(scheme) | "https".equals(scheme))) { throw new IllegalArgumentException("..."); } this.scheme = scheme; }
В данном коде использование оператора | неоправданно, так как при его использовании правая часть будет вычислена даже в случае, если левая часть уже является истинной. В данном случае это не имеет практического смысла, поэтому оператор | стоит заменить на ||.
Проект IntelliJ IDEA
Предупреждение PVS-Studio: V6008 Potential null dereference of 'editor'. IntroduceVariableBase.java:609
final PsiElement nameSuggestionContext = editor == null ? null : file.findElementAt(...); // <= final RefactoringSupportProvider supportProvider = LanguageRefactoringSupport.INSTANCE.forLanguage(...); final boolean isInplaceAvailableOnDataContext = supportProvider != null && editor.getSettings().isVariableInplaceRenameEnabled() && // <= ...
Анализатор обнаружил, что в данном коде может произойти разыменование нулевого указателя editor. Стоит добавить дополнительную проверку.
Предупреждение PVS-Studio: V6007 Expression is always false. RefResolveServiceImpl.java:814
@Override public boolean contains(@NotNull VirtualFile file) { .... return false & !myProjectFileIndex.isUnderSourceRootOfType(....); }
Мне сложно сказать, что имел в виду автор, но такой код выглядит весьма подозрительно. Даже если вдруг ошибки здесь нет, думаю, стоит переписать это место так, чтобы не смущать анализатор и других программистов.
Предупреждение PVS-Studio: V6007 Expression 'result[0]' is always false. CopyClassesHandler.java:298
final boolean[] result = new boolean[] {false}; // <= Runnable command = () -> { PsiDirectory target; if (targetDirectory instanceof PsiDirectory) { target = (PsiDirectory)targetDirectory; } else { target = WriteAction.compute(() -> ((MoveDestination)targetDirectory).getTargetDirectory( defaultTargetDirectory)); } try { Collection<PsiFile> files = doCopyClasses(classes, map, copyClassName, target, project); if (files != null) { if (openInEditor) { for (PsiFile file : files) { CopyHandler.updateSelectionInActiveProjectView( file, project, selectInActivePanel); } EditorHelper.openFilesInEditor( files.toArray(PsiFile.EMPTY_ARRAY)); } } } catch (IncorrectOperationException ex) { Messages.showMessageDialog(project, ex.getMessage(), RefactoringBundle.message("error.title"), Messages.getErrorIcon()); } }; CommandProcessor processor = CommandProcessor.getInstance(); processor.executeCommand(project, command, commandName, null); if (result[0]) { // <= ToolWindowManager.getInstance(project).invokeLater(() -> ToolWindowManager.getInstance(project) .activateEditorComponent()); }
Подозреваю, что здесь забыли как-либо изменить значение в result. Из-за этого анализатор сообщает о том, что проверка if (result[0]) является бессмысленной.
Заключение
Java направление является весьма разносторонним – это и desktop, и android, и web, и многое другое, поэтому у нас есть большой простор для деятельности. В первую очередь, конечно, будем развивать те направления, что будут наиболее востребованы.
Вот наши планы на ближайшее будущее:
- Вывод аннотаций на основе байт-кода;
- Интеграция в проекты на Ant (кто-то его еще использует в 2018?);
- Плагин для Eclipse (в процессе разработки);
- Еще больше диагностик и аннотаций;
- Совершенствование механизма Data Flow.
Также предлагаю желающим поучаствовать в тестировании альфа-версии нашего Java анализатора, когда она станет доступной. Для этого напишите нам в поддержку. Мы внесем ваш контакт в список и напишем вам, когда подготовим первую альфа-версию.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Egor Bredikhin. Development of a new static analyzer:
Прочитали статью и есть вопрос?
Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.

