R&D вокруг PVS-Studio




У нас есть большой список задач и пожеланий, которого мы придерживаемся при разработке PVS-Studio. Но иногда мы тратим время на необычные эксперименты, которые могут дать новые направления развития и возможности. Результаты исследования могут быть удачными и тогда они будут включены в основной продукт. А могут оказаться бестолковыми. И тогда, после нескольких экспериментов мы узнаем еще одну вещь, которая не работает. Сегодняшний рассказ как раз о таких экспериментах.

Введение

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

  • PVS-Studio использует сторонний препроцессор для своей работы. Раньше это был препроцессор из Visual C++. Сейчас в большинстве случаев Clang, хотя иногда приходится использовать по-прежнему Visual C++.
  • PVS-Studio использует файл проекта .vcproj/.vcxproj для того, чтобы получить информацию о настройках проекта. Например, используемые #define/#include, ключи компиляции, которые могут повлиять на анализ кода и т.п.
  • Кроме этого, для PVS-Studio файл проекта .vcproj/.vcxproj еще нужен и для того, чтобы знать какие файлы проверять.

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

Что уже есть?

Сначала я расскажу о том, что уже давно есть и работает в PVS-Studio.

Во-первых, в PVS-Studio довольно давно есть возможность проверять проекты, с интеграцией в системы сборки (как классические makefile, так и более крутые). Это описано в документации, этим активно пользуются некоторые наши клиенты. В этом случае как таковая среда Visual Studio остается почти за бортом.

Во-вторых, вместо препроцессора cl.exe опять же давно можно использовать Clang, который идет в дистрибутиве PVS-Studio. Причем по умолчанию сейчас используется именно Clang. Это делается из-за того, что Clang работает быстрее в качестве препроцессора, чем cl.exe. Кроме того, в Clang нет некоторых ошибок, имеющихся в cl.exe. Правда есть некоторые другие, которых в cl.exe нет. Но для пользователя всё это выглядит довольно прозрачно.

Какие эксперименты мы ставили?

Вот список вопросов, на которые мы хотели узнать ответ в своих экспериментах:

  • Нужна ли для правильного статического анализа кода структура проекта, определённая в makefile или .vcxproj файлах? Важны ли индивидуальные параметры компиляции каждого конкретного файла? Нельзя ли обойтись командами типа: "Проверить все файлы в такой-то папке с одинаковыми для всех ключами сборки?"
  • Нужны ли учитывать настройки параметров компиляции (речь о ключах компилятора) для статического анализа?
  • Нужно ли вообще выполнять препроцессирование файлов для статического анализа или можно найти ошибки и без него.

Для того чтобы ответить на эти вопросы мы написали утилиту, которая обходит рекурсивно указанную папку и запускает PVS-Studio.exe на все файлы с исходным кодом (*.c, *.cpp, *.cxx и т.д.). Мы хотели сравнить результаты анализа, полученные таким образом с результатами, полученными с помощью традиционной проверки проекта из Visual Studio.

Эксперимент первый, нужна ли структура проекта?

Начали мы экспериментировать на проекте WinMerge, который уже проверяли довольно давно. Если проверять его из Visual Studio используя файл проекта, то PVS-Studio проверит примерно 270 файлов, включенных в .vcproj. Что интересно, всего в папке с WinMerge лежит около 500 файлов, с исходным кодом (без .h-файлов). Это хоть и очевидно сейчас, но, все равно, для нас было неожиданно. Получается, что если сказать статическому анализатору: "Проверь мне файлы из этой папки", то анализатор напроверяет лишнего! Причем в случае с WinMerge файлов больше почти в два раза.

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

Но это была не основная проблема. Когда мы запустили процесс анализа для всех файлов, то сразу же полезли ошибки препроцессора: "Не могу найти #include-файл такой-то". С указанием имени .h-файлов из проекта, находящихся в других папках. Стало ясно, что нужно указать препроцессору папки с include-файлами. Как это сделать в автоматическом режиме, без ручного прописывания таких папок? Для каждого файла мы добавляли все подпапки в список #include-директорий.

Забегая вперед замечу, что не для всех проектов это удается сделать легко. Если в проекте тысячи подпапок, то автоматическое добавление их в список для поиска #include-файлов приводит к разрастанию командной строки препроцессора. И если для cl.exe еще можно использовать response file, то для Clang пока проблема не решаемая.

Так вот, если автоматически подставить все подпапки в список для поиска #include-файлов, то возникает другая проблема.

Эта проблема заключается в том, что в проектах бывают файлы с одинаковыми именами. И автоматически определить файлы из какой папки и для какого проекта надо использовать в каждом конкретном #include-поиске нельзя. Вы скажите: "Ну да, иногда, наверное, очень редко, встречаются проекты, где есть файлы с одинаковыми именами. Но ими можно пренебречь!" А вот и нет. Например, среди проектов для Visual Studio почти все проекты содержат файлы с одинаковыми именами. Не верите? Думаете, в ваших проектах нет файлов с одинаковыми именами? Тогда поищите-ка stdafx.h в своих проектах... А поскольку файл stdafx.h должен включаться во все файлы, то выбор неверной версии stdafx.h приводит к ошибке препроцессирования для ВСЕХ файлов проекта.

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

Выводы по результатам первого эксперимента можно сделать следующие. Проверка "всех файлов в папке" без какого-либо файла проекта (будь то makefile или vcproj) затруднительна по двум причинам:

  • В папке почти всегда есть дополнительные "лишние" файлы, которых часто бывает много. Они при этом могут не компилироваться, быть некорректными или просто "замусоривать" выдачу анализатора.
  • Задача индивидуального указания #include ключей для каждого файла может быть решена только вручную. Решить её просто передав каждому файлу одинаковый набор ключей нельзя из-за того, что часто проекты содержат файлы с одинаковыми именами.

Эксперимент второй, о пользе ключей компиляции

Вообще, немалое количество ключей компиляции на первый взгляд влияют на то, как файл будет восприниматься с точки зрения статического анализа. Но кроме очевидных и только что рассмотренных путей до #include-файлов, а также #define-директив, влияющих на включение веток кода есть много других параметров. Насколько влияют они?

Например, есть ключ "/J" в компиляторе cl.exe:

  • /J (Default char Type Is unsigned)

Параметр вроде бы важный, но насколько он влияет на статический анализ? Или, к примеру, еще параметры, относящиеся к расширениям языка:

  • /Za, /Ze (Disable Language Extensions)

Для того чтобы проверить влияние подобных параметров мы сравнивали результаты анализа проекта в обычном (традиционном) режиме и такой же проверки без учета подобных параметров компиляции.

Эксперимент показал крайне незначительное отличие результатов, которые были получены без учета параметров компиляции. Более того, даже отсутствие передаваемых обычно #define-параметров почти не повлияло в целом на качество анализа. Конечно же, анализатор неправильно выбирал ветки кода в #ifdef-конструкциях. Но это ожидаемо и логично. Единственный параметр, который на 100% необходим – это все-таки пути до #include-файлов.

Выводы по результатам второго эксперимента – желательно учитывать параметры компиляции для того, чтобы результаты статического анализа были наиболее точны. Однако если по каким-либо причинам сделать это не удается (например, сложная система сборки), то можно попробовать обойтись без них. Исключением является параметр, задающий путь до #include-файлов – он необходим.

Эксперимент третий, а нужно ли препроцессирование?

Наконец, мы захотели проверить. А насколько реально все-таки нужно препроцессирование для того, чтобы получить качественные результаты статического анализа? Все-таки немало ошибок можно выявить с помощью "локального" анализа, т.е. внутри одной функции.

Для того чтобы понять это мы сделали следующий эксперимент. В анализаторе мы полностью отключили препроцессирование. И подсовывали в PVS-Studio.exe исходные .cpp-файлы "как есть" без какого либо препроцессирования. Затем сравнили результаты с эталонными.

Оказалось, что отказ от препроцессирования чрезвычайно губительно сказывается на качестве анализа. Ведь отказываясь от препроцессирования (отдельным этапом или "на лету"), мы теряем информацию о типах данных, классах, функциях, которые объявлены в .h-файлах. Из-за этого отваливается довольно большое количество наших диагностик. Да, кое-что находится. Но, во-первых, намного меньше, чем находилось до этого. А, во-вторых, появляется очень много мусорных сообщений, которые выдаются из-за того, что анализатор "не смог понять тип" данных и посчитал, что это может привести к проблемам.

Были ли реальные ошибки среди результатов, полученных без препроцессирования? Да были. Но их очень сложно отфильтровать среди заведомо большего количества ложных сообщений.

Выводы по результатам третьего эксперимента – отсутствие препроцессирования крайне губительно сказывается на качестве статического анализа. Хотя кое-какие ошибки все-таки удается выявить даже без препроцессора, тем не менее, отсутствие информации о типах данных, о заголовках функций и тому подобных сведений существенно искажают результаты анализа и приводят к появлению мусорных сообщений. А значит статический анализ без препроцессирования не целесообразен.

Заключение

По результатам этого эксперимента мы пришли к выводам (или убедились в том, что предполагали и раньше):

  • Структура проекта очень важна, проверить просто "все файлы в папке" затруднительно. Во-первых, будут проверены лишние файлы. Во-вторых, из-за невозможности автоматически генерировать без какого-либо проектного файла индивидуальные параметры сборки для каждого файла возникнет путаница при включении заголовочных файлов с одинаковыми именами.
  • Ключи компиляции желательно учитывать, но их влияние не столь критично. За исключением путей до #include-файлов и, возможно #define-параметров.
  • Препроцессирование является необходимым этапом для статического анализа. Без него теряется существенная часть информации о структуре кода и из-за этого результаты анализа получаются некачественными.

Поэтому мы вряд ли в ближайшее время откажемся от устоявшейся схемы проверки проектов.



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

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

goto PVS-Studio;


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

Проверено проектов
361
Собрано ошибок
13 417

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

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

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

goto PVS-Studio;