[Из песочницы] Verone — статический анализатор для C++ с анализом на лету

Меня зовут Владимир Пляшкун и в сегодняшней статье я собираюсь представить бесплатный статический анализатор Verone для C++03/C++11/C++14, главной особенностью которого является анализ на лету.
На сегодняшний день имеется большой выбор инструментов статического анализа для языка C++ это Coverity/Klocwork/Parasoft/Clang/CppCheck/PVS-Studio и многие другие. А недавно еще вышел ReSharper C++.

Тем не менее, задача анализа на лету по-прежнему актуальна, т.к. практически нет решений обладающих данной особенностью. Тот же ReSharper хоть и интегрируется с Visual Studio и находит многие проблемы онлайн (например, неиспользуемые заголовочные файлы), но он все же видится больше как средство рефакторинга, нежели классический статический анализатор. У R# практически нет диагностик семантических ошибок, copy-paste ошибок и опечаток, а также ошибок времени выполнения (утечки памяти, буферные переполнения, разыменовывания нулевых указателей и т.д.)

Coverity/Klocwork/Parasoft — каждый из этих инструментов называет себя лидером рынка, но получить даже пробную версию этих анализаторов индивидуальному разработчику будет крайне затруднительно, ибо целевая направленность у этих продуктов иная и направлена на большие компании. А потому, рассматривать их нет смысла.

Clang, в основном направлен на поиск уязвимостей в исходной программе. Доступные проверки приведены здесь.

Огорчает, что clang практически не диагностирует опечатки и семантические ошибки, да и использовать clang на данный момент за пределами XCode несколько неудобно. Конечно, анализатор можно запустить из консоли, но такая схема требует ручной настройки (указание списка файлов, пути к директориям заголовочных файлов, предопределенные макросы и пр.). А потому, такой периодический запуск внешней утилиты за пределами IDE вряд ли можно назвать удобным и полезным.

PVS-Studio пошел чуть дальше и интегрировался в систему сборки. Теперь анализ выполняется автоматически после каждой сборки проекта. Но опять же возникает вопрос, почему тогда не выполнять анализ на лету, подсвечивая все найденные ошибки прямо во время кодирования? Что гораздо логичнее и удобней для конечного пользователя.

Именно такую проблему и решает анализатор Verone. После установки анализатор будет автоматически стартовать при открытии Visual Studio. В фоновом потоке Verone будет отслеживать изменения, которые вносит программист в процессе работы, анализировать их, и выводить результат о найденных проблемах. Все диагностики для текущего файла указываются справа от редактора в специальном отступе:

7dc62be0d59a48c0aa13ef9a463b402e.png

Цвет, так называемого, глифа зависит от типа диагностики (заметка, предупреждение, ошибка). Чтобы посмотреть все найденные проблемы достаточно щелкнуть на иконку анализатора на панели инструментов, после чего появится окно со всеми обнаруженными проблемами:

1f140b73f3d241efa6a5377c3a6a5308.png

На данный момент анализатор имеет порядка 20 диагностик, направленные на поиск семантических ошибок и опечаток. Конечно, этого крайне мало, чтобы конкурировать с существующими инструментами, но стоит понимать, что это только первый релиз и многие наработки попросту не вошли в него. Планы на будущее будут в конце статьи. Тем не менее, этого уже достаточно, чтобы выявлять дефекты в реальных проектах.

Например, в исходном коде LLVM были найдены проблемы следующего характера:

else if (ArgTy->isStructureOrClassType())
    return record_type_class;
else if (ArgTy->isUnionType())   //<---
    return union_type_class;
else if (ArgTy->isArrayType())
    return array_type_class;
else if (ArgTy->isUnionType())  //<----
    return union_type_class;


Здесь условие

ArgTy->isUnionType()


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

if ((NDesc & MachO::REFERENCE_TYPE) == MachO::REFERENCE_FLAG_UNDEFINED_LAZY)  //<----
    outs() << "undefined [lazy bound]) ";
else if ((NDesc & MachO::REFERENCE_TYPE) == MachO::REFERENCE_FLAG_UNDEFINED_LAZY)  //<---
    outs() << "undefined [private lazy bound]) ";
else if ((NDesc & MachO::REFERENCE_TYPE) == MachO::REFERENCE_FLAG_PRIVATE_UNDEFINED_NON_LAZY)
    outs() << "undefined [private]) ";
else
    outs() << "undefined) ";


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

(NDesc & MachO::REFERENCE_TYPE) == MachO:: REFERENCE_FLAG_PRIVATE_UNDEFINED_LAZY


Подобная проблема была обнаружена и проекте libsass.

if (lhs_tail->combinator() != rhs_tail->combinator()) return false;
if (lhs_tail->head() && !rhs_tail->head()) return false;
if (!lhs_tail->head() && rhs_tail->head()) return false;
if (lhs_tail->head() && lhs_tail->head()) {  //<---
    if (!lhs_tail->head()->is_superselector_of(rhs_tail->head())) 
        return false;
}


В этом фрагменте ошибочно последнее условие:

lhs_tail->head() && lhs_tail->head()


Опять же, код написан таким образом, что ошибка не давала о себе знать, но это не значит, что ее не нужно исправлять, что и было сделано авторами в одном из последних коммитов.

Было найдено множество подозрительных мест в графическом движке Torque 3D.

// Update accumulation texture if it changed.
// Note: accumulation texture can be NULL, and must be updated.
if ( passRI->accuTex != lastAccuTex )
{
    sgData.accuTex = passRI->accuTex;
    lastAccuTex = lastAccuTex;
    dirty = true;
}


Исходя из логики, указатель lastAccuTex (GFXTextureObject*) ссылается на последний обработанный объект passRI→accuTex, но из-за невнимательности переменной присваивается собственное значение.

А в этом фрагменте:

class ConnectionProtocol
{
public:
   ConnectionProtocol();
   virtual void processRawPacket(BitStream *bstream);
   virtual Net::Error sendPacket(BitStream *bstream) = 0;
   virtual void keepAlive() = 0;
   virtual void handleConnectionEstablished() = 0;
   virtual void handleNotify(bool recvd) = 0;
   virtual void handlePacket(BitStream *bstream) = 0;
        ...
};


Класс полиморфный, но имеет деструктор по умолчанию (не виртуальный). Потому, это может привести к утечкам ресурсов в дочерних классах, если при удалении будет использоваться указатель на базовый класс.

Есть подозрительные места с целочисленным делением, отбрасывающее дробную часть, как например здесь:

bm->avgfloat=PACKETBLOBS/2


Переменная bm→avgfloat имеет тип с плавающей точкой, но поскольку справа от оператора присваивания стоит целочисленное деление, дробная часть будет отброшена. Не сказать что это однозначная ошибка, но внимания требует.

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

  1. Более продвинутый алгоритм анализа на лету, основанный на генерации mock-файлов (спасибо Саше Коковину за идею)
    Сейчас данный алгоритм работает крайне просто. По сути, все завязано вокруг файла с парами <имя файла> — <дата последнего анализа>. А ядро анализатора просто запускается с некоторой периодичностью. Поскольку в процессе работы изменяется небольшое количество файлов, такая схема все же позволила масштабироваться на большие проекты.
  2. Возможность анализировать еще несохраненные изменения в редакторе
    Вышеописанная схема, к сожалению, завязана на файлах и не учитывает изменения, которые уже сделаны в редакторе, но еще не сохранены. Данная задача достаточно объемная и требует тщательного тестирования, а потому не была реализована в первом релизе.
  3. Анализ ошибок времени выполнения
    Одним из основных направлений является и анализ ошибок времени выполнения, которого нет в текущей версии. Анализ ошибок времени выполнения вкупе с анализом на лету выглядит очень перспективно, ибо видеть, например, разыменовывание нулевого указателя в момент кодирования, по-моему, очень круто! Безусловно, придется пойти на некоторые упрощения, ибо алгоритмы анализа потока данных достаточно ресурсоемки. Но даже, например, локальный анализ все-равно будет приносить ощутимую пользу, предупреждая разработчика о возможных проблемах.


Из более отдаленных задач стоит выделить также:

  1. Улучшение графического интерфейса
    Сейчас нет функционала для скрытия диагностик, их поиска и сохранения в файл, что было бы полезно в повседневной работе.
  2. Освоение иных IDE и операционных систем
  3. Standalone приложение
    Самостоятельное графическое приложение полезно, т.к. не приковано ни к какой среде разработки. Уже сейчас можно спокойно использовать консольную версию анализатора, но версия с графическим интерфейсом видится более удобным решением для этой цели.


Анализатор доступен для загрузки по ссылке. На данный момент анализатор доступен только для Windows, доступна версия для интеграции с Visual Studio 2015. Версии для Visual Studio 2012 и Visual Studio 2013 появятся в течение недели-двух.

Готов ответить на любые вопросы. Буду рад обратной связи и пожеланиям!

© Habrahabr.ru