Как добавить новое диагностическое правило в PVS-Studio? Будни разработчиков...




Вопрос о том, как добавить собственное диагностическое правило в наш статический анализатор PVS-Studio задают нам довольно часто. И мы всегда отвечаем, что сделать это очень просто: "Нужно всего лишь написать нам письмо, и мы добавим это правило в анализатор". Такой интерфейс для добавления новых правил удобен для пользователя. Это самый лучший и удобный интерфейс. Сделать самому подобную работу, не так просто, как кажется. В заметке я покажу подводную часть айсберга, которая скрывается за понятием "добавили вот это простое правило".

Picture 1

Давайте сразу на чистоту. В PVS-Studio нет механизма для того, чтобы пользователь мог написать свои правила сам. Это, прежде всего, архитектурное ограничение. И некоторых пользователей это может расстроить. Однако на самом деле такие пользователи не понимают, что хотят. Потому что в жизни процесс добавления нового правила довольно сложен.

Вот есть, к примеру, правило V536: Be advised that the utilized constant value is represented by an octal form ("Проверьте правильность использования восьмеричной константы"). Оно предназначено для выявления таких ошибок, как например эта в Miranda IM:

static const struct _tag_cpltbl
{
  unsigned cp;
  const char* mimecp;
} cptbl[] =
{
  {   037, "IBM037" },       // IBM EBCDIC US-Canada 
  {   437, "IBM437" },       // OEM United States 
  {   500, "IBM500" },       // IBM EBCDIC International 
  {   708, "ASMO-708" },     // Arabic (ASMO 708) 
  ...
}

Здесь написано 037 вместо 37 просто для выравнивания, хотя получилось десятичное 31, так как 037 по правилам C++ - восьмеричная запись. Ошибка довольно простая для диагностирования, не так ли? В самом деле, ищем константу, смотрим, начинается ли она с 0 и выдаем диагностическое сообщение. Такое диагностическое правило можно реализовать за час. И пользователь статического анализатора (профессионал в программировании, но новичок в разработке подобных инструментов) готов в теории сам такое правило реализовать.

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

Мы же (разработчики PVS-Studio) поступаем следующим образом. Перед тем как реализовывать правило, мы сначала пишем на него юнит-тесты, где размечаем специальным образом строки, на которое оно должно срабатывать, а также оставляем в юнит-тесте строки, где правило не должно ничего выдавать. Затем пишется первая версия кода для диагностики. Добившись прохождения юнит-тестов, написанных для этого нового правила, мы запускаем уже полные юнит-тесты. Ведь новое правило могло что-то сломать. Полные юнит-тесты проходят за несколько десятков минут. Конечно же, с первого раза "ничего не сломать" удается далеко не всегда, поэтому итерации повторяются до полного прохождения юнит-тестов.

После юнит-тестов идет следующий этап – запуск на реальных проектах. В нашей тестовой базе существует около 70 проектов (на 2011 год). Это известные и не очень open source проекты, на которых мы тестируем свой анализатор. У нас есть своя самописная запускалка, которая открывает проекты в Visual Studio, запускает анализатор, сохраняет лог, сравнивает его с эталоном и показывает отличия (какие сообщения пропали, какие добавились, а какие изменились). Один цикл прогона всех проектов для Visual Studio 2005 длится 4 часа на машине с четырьмя ядрами и восемью гигабайтами памяти (анализ распараллелен). По результатам анализа мы видим результаты внедрения нового правила. Обычно, мы начинаем смотреть отличия не дожидаясь завершения анализа, так как иногда сразу видно, что "все сломалось". Однако даже если все работает и ничего не сломано, то нередко тесты останавливаются раньше. Причина этого – ложные срабатывания. Если мы видим, что у диагностического правила слишком много срабатываний, то в правиле начинают появляться исключения.

Вернемся к поиску некорректных восьмеричных чисел – правилу V536. В нем есть целый ряд исключений. Вот ситуации, когда диагностика не выдается:

  • Значение константы меньше 8.
  • Число объявлено через #define.
  • В одном блоке более двух восьмеричных чисел (правило сработало более двух раз), кроме 0, типа char.
  • Это число используется как аргумент в функциях: _open, mknod, open, wxMkdir, wxFileName::Mkdir, wxFileName::AppendDir, chmod.
  • Это вложенный блок чисел. Мы проверяем только самый верхний. Пример: { {090}, {091}, {092}, {093} }.
  • Это число <= 0777 и является частью высказывания, где есть слово: file, File.
  • Число является аргументом функции, в одном из параметров которой содержится строка "%o".

Предугадать исключения до запуска на реальных проектах можно не всегда. Поэтому итерация "исключение - прогон тестов – анализ результатов" может повторяться много раз.

Затем, когда все исключения реализованы, а количество ложных срабатываний является адекватным, результаты тестов (сохранные лог-файлы обнаруженных ошибок) объявляются эталонными. После чего выполняется прогон тех же тестов для других версий Visual Studio. Сейчас (2011) у нас поддерживаются три версии Visual Studio 2005/2008/2010, тесты в них выполняются соответственно 4/4.5/5 часов, что дает в сумме 14-15 часов времени. По результатам прогона в другой версии Visual Studio может выясниться, что на один и тот же код выдается разное количество диагностических сообщений в зависимости от версии Visual Studio. Это связано, как правило, с отличиями в заголовочных файлах различных SDK. Отличия мы стараемся устранить, чтобы прогон тестов во всех версиях Visual Studio давал одинаковые результаты.

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

О каких странных конструкциях идет речь? Приведу пару примеров.

Знаете ли вы, что можно объявить переменную, например, как-то вот так:

int const unsigned static a22 = 0;

Знаете, что __int64 может быть именем переменной?

int __identifier(__int64);

Знаете, что после switch фигурные скобки не обязательны?

switch (0)
  if (X) Foo();

Даже если вы сами не используете подобное конструкции в своем коде, они есть в используемых библиотеках.

Наконец, пройдя все тесты можно браться за документацию. На каждое правило диагностики мы пишем справку на русском и английском языках. Перевод на английский также занимает время. Теперь (когда код отлажен, тесты пройдены, документация написана) правило попадет в следующий релиз PVS-Studio.

Итак, цикл разработки нового диагностического правила можно коротко представить так:

  • Идея нового правила. Или мы придумываем, или пользователи подсказывают, или подсмотреть где-то удается.
  • Формулирование правила.
  • Реализация правила.
  • Всевозможное тестирование.
  • Анализ результатов. Возможны варианты:
    • Доработка формулировки правила, ввод исключений, переход к пункту 2.
    • Реализация правила считается устоявшейся, можно писать и переводить документацию.

Общее время реализации одного правила составляет несколько дней. Как правило, это от 2 до 5 дней. Естественно, что с опытом мы уже можем предугадывать, какие понадобятся исключения для правила и сразу же их реализовывать. Да и количество прогонов тестов удается сокращать. Однако в любом случае, ключевой момент заключается в том, что только на основе тестовых прогонов можно сформулировать исключения и побороть большую часть ложных срабатываний.

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

Тогда возникает вопрос: "Почему же в некоторых известных и уважаемых статических анализаторах есть способы для задания собственных пользовательских правил?" Может быть, мы просто не можем такого сделать? Отчасти да. Но только механизм задания собственных правил служит для одной просто цели – для составления "матриц сравнения с конкурентами":

Рисунок 1

И что же, спросит читатель, в PVS-Studio никогда нельзя будет создать пользовательское правило? Когда-нибудь у нас хватит сил, чтобы реализовать такое и не проигрывать в матрицах сравнения :-). А пока мы будем всем кто хочет создать новое правило давать ссылку на этот текст и говорить: "У нас самый лучший интерфейс для добавления пользовательских правил – просто напишите нам письмо!"



Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;


Найденные ошибки

Проверено проектов
346
Собрано ошибок
13 188

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;