На данный момент анализатор PVS-Studio уже имеет механизм для подавления ложных срабатываний (False Positive). Этот механизм полностью устраивает нас с функциональной точки зрения, т.е. у нас нет претензий к надёжности его работы. Однако, у некоторых из наших пользователей и клиентов возникало желание иметь возможность работать с сообщениями анализатора только на «новом», т.е. вновь написанном коде. Это желание вполне можно понять, учитывая, что в крупном проекте анализатор может сгенерировать тысячи или даже десятки тысяч сообщений на существующий код, править которые, конечно, никто не станет.
Возможность разметки сообщений, как «ложных» в каком-то смысле пересекается с желанием работать только с «новыми» сообщениями, т.к. ничто, теоретически, не мешает разметить все найденные сообщения, как «ложные», и в дальнейшем работать только с сообщениями на вновь написанном коде.
Однако, в существующем механизме для разметки «ложных сообщений» существует принципиальная usability проблема (о чём мы поговорим далее), которая может стать преградой при его использования на реальных проектах для решения данной задачи. В частности, этот существующий механизм не подразумевает использования для «массовой» разметки, что будет неизбежно при обработке тысяч сообщений анализатора.
В связи с тем, что описанная выше проблема является фундаментальной для существующей методики, её невозможно устранить с сохранением этой же методики. Поэтому имеет смысл рассмотреть возможность реализации альтернативного способа для решения данной задачи.
Задача подавления ложных срабатываний и задача подавления результатов предыдущих запусков.
Механизм сопоставления исходного кода и диагностик анализатора подразумевает возможность сопоставить строку исходного кода с определённой диагностикой анализатора. При это важно сохранение данной связи в течение длительного промежутка времени, за который как код пользователя, так и диагностический вывод анализатора, могут изменяться.
Механизм сопоставления исходного кода и диагностик может быть использован для решения 2-х задач:
- Задача подавления ложных срабатываний. Пользователю предоставляется возможность пометить сообщения анализатора, которые тот считает ложными, специальным образом, что в последствии позволит ему осуществлять фильтрацию таких сообщений (например, скрывать их). Эта разметка должна сохраниться и при последующих запусках анализатора, в том числе и после того, как исходный код будет изменён. Данный функционал уже достаточно давно доступен в PVS-Studio.
- Задача подавления результатов предыдущих запусков заключается в предоставлении пользователю возможности видеть только «свежие» результаты запуска анализатора (т.е. результаты, которые уже были найдены на предыдущих запусках, не должны отображаться). Данная задача не была ранее реализована в PVS-Studio, и именно о ней пойдёт речь в следующем разделе статьи.
PVS-Studio располагает механизмом сопоставления исходного кода и диагностик, основанном на маркерах (комментариях специального вида) в исходном коде. Механизм реализован на уровне ядра анализатора (PVS-Studio.exe) и IDE плагинов. IDE плагин осуществляет первоначальную расстановку маркеров в коде, а также позволяет фильтровать результаты анализа по этому маркеру. Ядро анализатора может «подхватывать» уже присутствующие в коде маркеры и размечать свой вывод, тем самым сохраняя разметку кода от предыдущих запусков.
Рассмотрим достоинства и недостатки существующего механизма.
Достоинства:
- Простота реализации на уровне ядра и плагинов
- Интуитивность использования для пользователей инструмента, возможность ручной разметки.
- Гарантия сохранения связи «код — диагностика» при любых последующих модификациях кода пользователем.
- «Бесплатная» поддержка командной разработки – т.к. маркеры хранятся в исходных файлах, для их синхронизации может использоваться та же система, что и для синхронизации самих файлов (например, система контроля версий)
Недостатки
- Засорение кода комментариями специального вида, не относящимися к логике работы этого кода.
- Проблема при использовании систем управления версиями, необходимость закладывать комментарии специального вида в репозиторий.
- Потенциальная возможность порчи исходного кода при глобальной массовой разметке.
Описанные выше проблемы делают невозможным с практической точки зрения использование существующего механизма сопоставления для реализации задачи подавления результатов предыдущих запусков, т.е. для «массовой» разметки сообщений на существующей кодовой базе.
Другими словами, никто не хочет не глядя добавить в код 20000 комментариев, подавляющих имеющиеся сообщения, и заложить все эти изменения в систему контроля версий.
Новый механизм сопоставления диагностик с исходным кодом основанный на файлах баз сообщений.
Как было показано ранее, главной проблемой существующего механизма является его завязанность на модификацию исходного кода пользователя. Из этого факта проистекают как свойственные такому подходу несомненные плюсы, так и его минусы. Очевидным становится, что для реализации альтернативного подхода необходимо отказаться от модификации кода пользователя, и хранить информацию о связке «диагностика анализатора – код пользователя» в некотором внешнем хранилище, а не в самих исходных файлах.
Долгосрочное хранение такой разметки сопоставлений ставит принципиальную задачу учёта изменений на большом временном промежутке как в диагностиках самого анализатора, так и в исходном коде пользователя. Исчезновение диагностики в выдаче анализатора не является принципиальной проблемой, так как сообщение с такой диагностикой уже было размечено, как ложноененужное. А вот изменения в коде пользователя могут привести к «второму пришествию» сообщений, которые раньше уже были размечены.
Данная проблема не страшна при использовании механизма разметки кода. Как бы сильно не изменился участок кода, маркер останется в нём до тех пор, пока пользователь сам (волевым решением или по незнанию) не удалит его, что кажется маловероятным. Более того, опытный пользователь может сам добавить такой маркер на новый (или изменившийся) участок кода, если он знает, что здесь анализатор будет ругаться.
Что именно требуется для идентификации диагностического сообщения анализатора? Само сообщение анализатора содержит имя файла, проект, номер строки в файле и контрольные суммы предыдущей, текущей и последующей строк кода, на которых эти диагностики были найдены. Для сопоставления диагностики при изменении в исходном коде однозначно необходимо будет не учитывать номер строки, т.к. он меняется непредсказуемо и при малейшей модификации документа.
Для реализации описанного выше хранилища «связей» диагностик с кодом пользователя мы пошли по пути создания локальных «файлов баз». Такие файлы (файлы с расширением suppress) создаются рядом с проектными файлами (vcprojvcxproj) и содержат в себе списки размеченных «ненужных» диагностик. Диагностики хранятся без учёта номеров строк, пути к файлам, в которых эти диагностики были идентифицированы, хранятся в относительном формате – относительно проектных файлов. Это позволяет переносить такие файлы между машинами разработчиков даже если проекты у них развёрнуты в разных местах (с точки зрения файловой системы). Эти файлы можно закладывать в системы контроля версий, ведь в большинстве случаев проектные файлы сами по себе хранят пути до исходных файлов в таком же относительном формате. Исключением тут являются генерируемые файлы проектов, как в случае с CMake, например, где «дерево исходников» может быть расположено независимо от «дерева проектов».
Мы использовали следующие поля для идентификации сообщения в suppress файле:
- Текст диагностического сообщения анализатора;
- Код ошибки сообщения;
- Относительное имя до файла, в котором сообщение было найдено;
- Хэш суммы строки c кодом, на которой сообщение было найдено, а также предыдущей и следующей строк кода.
Как видно, именно за счёт хранения хэш сумм строк исходного кода мы хотели бы соотносить сообщение анализатора с кодом пользователя. При этом, если код пользователя «сдвигается», то сдвинется и сообщение анализатора, однако «контекст» этого сообщения (т.е. код, который его окружает) останется неизменным. Если же пользователь правит свой код в месте, где сообщение было сгенерировано, то вполне логично считать такой код уже «новым», и показать сообщение анализатора на этот код. При этом, если пользователь реально «исправил» ошибку, на которую указывал своим сообщением анализатор, то сообщение просто «исчезнет». Иначе, если подозрительное место не исправлено – пользователь вновь увидит сообщение анализатора.
Понятно, что опираясь на хэши строк кода в файлах пользователя, мы столкнёмся с рядом ограничений. Например, если у пользователя есть несколько идентичных строк кода в файле, мы посчитаем все сообщения на такие строки подавленными, даже если было размечено только одно из них. Подробнее про проблемы и ограничения, с которыми мы столкнулись при использовании описанной методики, будет рассказано в следующем разделе.
IDE плагины PVS-Studio автоматически создают suppress файлы при первоначальной разметке сообщений, и в дальнейшем сопоставляют все вновь сгенерированные диагностики с теми, что содержатся в suppress базах. И, если после перепроверки вновь сгенерированное сообщение будет идентифицировано в базе, оно не будет показано пользователю.
Статистика по использованию нового механизма подавления
После реализации первого работоспособного прототипа нового механизма мы, естественно, захотели посмотреть, как этот механизм покажет себя при работе с реальными проектами. Мы не стали ждать несколько месяцевлет, пока в таких проектах накопятся достаточное количество изменений, а просто взяли несколько прошлых ревизий в нескольких крупных open source проектах.
Что мы хотели увидеть? Мы брали какую-то достаточно старую ревизию проекта (в зависимости от активности разработчиков, это могла быть и неделя, и целый год), проверяли её нашим анализатором, закладывали все полученные сообщений в suppress базы. Затем обновляли проект до его последней head ревизии и проверяли анализатором снова. В идеале мы должны были бы увидеть сообщений, найденные только на «новом» коде, т.е. коде, который был написан в рассматриваемый нами промежуток времени.
При проверке первого же проекта мы столкнулись с рядом проблем и ограничений нашей методики. Рассмотрим их поподробнее.
Во-первых, что, в принципе, было ожидаемо, сообщения «появлялись вновь» в случае, если модифицировался код в месте выдачи самого сообщения, либо на предыдущейследующей строке. При этом, если модификация строки самого сообщения вполне ожидаемо приводила к «воскрешению» такого сообщения, то модификация окружающих строк, как может показаться, к этому приводить не должна. Это, в частности, и является одним из основных ограничений выбранной нами методики – мы привязываемся к тексту исходного файла на этих 3-х строчках. Далее, привязываться только к одной строке кажется нецелесообразным – слишком много сообщений потенциально могут быть «перепутаны». В статистике по проектам, которая будет приведена далее, мы обозначим такие сообщения «парными» – т.е. сообщения, которые как бы уже есть в suppress базах, но всплыли вновь.
Во-вторых, выяснилась ещё одна особенность (а точнее – очередное ограничение) нашего нового механизма – «воскрешение» сообщений в h (заголовочных) файлах в случае, когда эти файлы включались в другие исходные файлы, в других проектах. Это ограничение связано с тем, что базы генерируются на уровне IDE проекта. Аналогичная ситуация возникает и в случае появления новых проектов в решении, переиспользующих заголовочныеисходные файлы.
Далее, оказалось не очень хорошей идеей ориентироваться на текст сообщения анализатора для идентификации такого сообщения в базах. Иногда текст сообщения анализатора может содержать в себе номера строк в исходном коде (они меняются в случае сдвига) и имена переменных, фигурирующих в коде пользователя. Мы решили данную проблему, сохраняя в базе не полное сообщение анализатора – из него вырезаются все цифровые символы. А вот «воскрешение» сообщения при изменении имени переменной мы решили посчитать корректным – ведь могло поменяться не только её имя, но и определение – мы считаем это уже «новым» кодом.
Наконец, некоторые сообщения «мигрировали» – то ли код с ними был скопирован в другие файлы, то ли файлы включались в другие проекты, что, в принципе, пересекается с самой первой описанной проблемой нашей методики.
Перечислим статистику по нескольким проектам, на которых мы тестировали новую систему. Большое количество диагностических сообщений вызвано тем, что учитывались вообще все сообщения. В том числе диагностика 64-битных ошибок, которая к сожалению, вообще генерирует много ложных срабатываний и с этим ничего нельзя поделать.
- LLVM –большой проект для универсальной системы анализа, трансформации и оптимизации программ. Проект уже не один год активно развивается, соответственно, достаточно было взять динамику изменений всего за 1.5 месяца, чтобы получить большое количество модификаций кода. Хорошо известный компилятор Clang является частью данного проекта. Для 1600-1800 файлов проекта было помечено 52 000 сообщений как ненужные. Через 1.5 месяца было обнаружено 18 000 новых сообщений анализатора, из них 500 парных и 500 сообщений, мигрировавших в другие файлы;
- Miranda — всем известная программа обмена сообщениями для ОС Windows. Miranda насчитывает 11 версий со времен своего первого выпуска. Мы взяли самую последнюю из них: Miranda NG. К сожалению, из-за конфликтов в команде разработчиков Miranda, данная версия менялась не слишком часто: пришлось брать изменения с интервалом аж в 2 года. Для 700 файлов было помечено 51 000 сообщение как ненужные. Спустя два года появилось всего 41 новое сообщение;
- ffdShow — медиа декодер, обычно используемый для быстрого и высокоточного декодирования видеопотоков. ffdShow достаточно законченный проект, на момент написания статьи, последний релиз был в Апреле 2013 года. Мы взяли динамику изменений за 1 год. Из 570 файлов 21 000 сообщений было помечено как ненужные. Через год появилось 120 новых сообщений;
- Torque3D – игровой движок. Сейчас проект практически не развивается, но по началу все было иначе. Последний релиз, на момент написания статьи, был 1 Мая 2007 года. На момент активных разработок динамика изменений с интервалом в неделю выдала 43259 сообщений. За этот промежуток времени появилось 222 новых;
- OpenCV — библиотека алгоритмов компьютерного зрения, обработки изображений и численных алгоритмов общего назначения. Достаточно динамично развивающийся проект. Мы взяли динамику изменений за 2 месяца и за год. Было помечено 50948 ненужных сообщений. Из них 1174 новых сообщений по истечении 2-х месяцев и 19471 спустя год;
Какие же выводы мы можем сделать из полученных нами результатов?
Вполне ожидаемо, что на проектах, не развивающихся активно, мы не увидели большого числа «новых» сообщений, даже на таком большом отрезке времени, как целый год. Заметим, что для таких проектов мы не стали считать количество «парных» и мигрировавших сообщений.
Но наибольший интерес, безусловно, для нас представляют «живые проекты». В частности, на примере LLVM мы видим, что количество «новых» сообщений составило 34% от помеченных на версии, отстающей по времени всего на 1.5 месяца! Тем не менее, из этих 18 000 новых сообщений только 1000 (500 мигрировавших + 500 парных) относятся к ограничениям нашей методики, т.е. всего 5% от общего числа новых сообщений.
На наш взгляд, эти цифры очень хорошо продемонстрировали жизнеспособность нового механизма. Конечно, стоит помнить, что новый механизм подавления ни в коем случае не является «панацеей», но ничто не отменяет возможность использовать многочисленные уже существующие ранее методы подавленияфильтрации. Например, если какое-то сообщение в h файле начинает очень часто «всплывать», не будет ничего плохого в том, чтобы «убить» его навсегда, добавив к строке комментарий вида //-Vxxx.
Несмотря на то, что новый механизм уже достаточно отлажен, и мы готовы его показать нашим пользователям в следующем релизе, мы решили продолжать его тестировать, организовав регулярную (каждую ночь) проверку проекта LLVM/Clang. Новый механизм позволит нам смотреть только на сообщения из «свежего» кода проекта – теоретически мы сможем находить ошибки ещё до того, как их обнаружат у себя разработчики. Это очень хорошо покажет реальную пользу от регулярного использования статического анализа – и это было бы невозможно без нашей новой системы подавления, ведь нереально просматривать по 50 000 каждый день. Ждите отчётов о найденных свежих багах в Clang в нашем твиттере.
Автор: Paull