На данный момент анализатор PVS-Studio уже имеет механизм для подавления ложных срабатываний (False Positive). Этот механизм полностью устраивает нас с функциональной точки зрения, т.е. у нас нет претензий к надёжности его работы. Однако, у некоторых из наших пользователей и клиентов возникало желание иметь возможность работать с сообщениями анализатора только на "новом", т.е. вновь написанном коде. Это желание вполне можно понять, учитывая, что в крупном проекте анализатор может сгенерировать тысячи или даже десятки тысяч сообщений на существующий код, править которые, конечно, никто не станет.
Возможность разметки сообщений, как "ложных" в каком-то смысле пересекается с желанием работать только с "новыми" сообщениями, т.к. ничто, теоретически, не мешает разметить все найденные сообщения, как "ложные", и в дальнейшем работать только с сообщениями на вновь написанном коде.
Однако, в существующем механизме для разметки "ложных сообщений" существует принципиальная usability проблема (о чём мы поговорим далее), которая может стать преградой при его использования на реальных проектах для решения данной задачи. В частности, этот существующий механизм не подразумевает использования для "массовой" разметки, что будет неизбежно при обработке тысяч сообщений анализатора.
В связи с тем, что описанная выше проблема является фундаментальной для существующей методики, её невозможно устранить с сохранением этой же методики. Поэтому имеет смысл рассмотреть возможность реализации альтернативного способа для решения данной задачи.
Механизм сопоставления исходного кода и диагностик анализатора подразумевает возможность сопоставить строку исходного кода с определённой диагностикой анализатора. При это важно сохранение данной связи в течение длительного промежутка времени, за который как код пользователя, так и диагностический вывод анализатора, могут изменяться.
Механизм сопоставления исходного кода и диагностик может быть использован для решения 2-х задач:
PVS-Studio располагает механизмом сопоставления исходного кода и диагностик, основанном на маркерах (комментариях специального вида) в исходном коде. Механизм реализован на уровне ядра анализатора (PVS-Studio.exe) и IDE плагинов. IDE плагин осуществляет первоначальную расстановку маркеров в коде, а также позволяет фильтровать результаты анализа по этому маркеру. Ядро анализатора может "подхватывать" уже присутствующие в коде маркеры и размечать свой вывод, тем самым сохраняя разметку кода от предыдущих запусков.
Рассмотрим достоинства и недостатки существующего механизма.
Достоинства:
Недостатки
Описанные выше проблемы делают невозможным с практической точки зрения использование существующего механизма сопоставления для реализации задачи подавления результатов предыдущих запусков, т.е. для "массовой" разметки сообщений на существующей кодовой базе.
Другими словами, никто не хочет не глядя добавить в код 20000 комментариев, подавляющих имеющиеся сообщения, и заложить все эти изменения в систему контроля версий.
Как было показано ранее, главной проблемой существующего механизма является его завязанность на модификацию исходного кода пользователя. Из этого факта проистекают как свойственные такому подходу несомненные плюсы, так и его минусы. Очевидным становится, что для реализации альтернативного подхода необходимо отказаться от модификации кода пользователя, и хранить информацию о связке "диагностика анализатора – код пользователя" в некотором внешнем хранилище, а не в самих исходных файлах.
Долгосрочное хранение такой разметки сопоставлений ставит принципиальную задачу учёта изменений на большом временном промежутке как в диагностиках самого анализатора, так и в исходном коде пользователя. Исчезновение диагностики в выдаче анализатора не является принципиальной проблемой, так как сообщение с такой диагностикой уже было размечено, как ложное\ненужное. А вот изменения в коде пользователя могут привести к "второму пришествию" сообщений, которые раньше уже были размечены.
Данная проблема не страшна при использовании механизма разметки кода. Как бы сильно не изменился участок кода, маркер останется в нём до тех пор, пока пользователь сам (волевым решением или по незнанию) не удалит его, что кажется маловероятным. Более того, опытный пользователь может сам добавить такой маркер на новый (или изменившийся) участок кода, если он знает, что здесь анализатор будет ругаться.
Что именно требуется для идентификации диагностического сообщения анализатора? Само сообщение анализатора содержит имя файла, проект, номер строки в файле и контрольные суммы предыдущей, текущей и последующей строк кода, на которых эти диагностики были найдены. Для сопоставления диагностики при изменении в исходном коде однозначно необходимо будет не учитывать номер строки, т.к. он меняется непредсказуемо и при малейшей модификации документа.
Для реализации описанного выше хранилища "связей" диагностик с кодом пользователя мы пошли по пути создания локальных "файлов баз". Такие файлы (файлы с расширением suppress) создаются рядом с проектными файлами (vcproj\vcxproj) и содержат в себе списки размеченных "ненужных" диагностик. Диагностики хранятся без учёта номеров строк, пути к файлам, в которых эти диагностики были идентифицированы, хранятся в относительном формате – относительно проектных файлов. Это позволяет переносить такие файлы между машинами разработчиков даже если проекты у них развёрнуты в разных местах (с точки зрения файловой системы). Эти файлы можно закладывать в системы контроля версий, ведь в большинстве случаев проектные файлы сами по себе хранят пути до исходных файлов в таком же относительном формате. Исключением тут являются генерируемые файлы проектов, как в случае с CMake, например, где "дерево исходников" может быть расположено независимо от "дерева проектов".
Мы использовали следующие поля для идентификации сообщения в suppress файле:
Как видно, именно за счёт хранения хэш сумм строк исходного кода мы хотели бы соотносить сообщение анализатора с кодом пользователя. При этом, если код пользователя "сдвигается", то сдвинется и сообщение анализатора, однако "контекст" этого сообщения (т.е. код, который его окружает) останется неизменным. Если же пользователь правит свой код в месте, где сообщение было сгенерировано, то вполне логично считать такой код уже "новым", и показать сообщение анализатора на этот код. При этом, если пользователь реально "исправил" ошибку, на которую указывал своим сообщением анализатор, то сообщение просто "исчезнет". Иначе, если подозрительное место не исправлено – пользователь вновь увидит сообщение анализатора.
Понятно, что опираясь на хэши строк кода в файлах пользователя, мы столкнёмся с рядом ограничений. Например, если у пользователя есть несколько идентичных строк кода в файле, мы посчитаем все сообщения на такие строки подавленными, даже если было размечено только одно из них. Подробнее про проблемы и ограничения, с которыми мы столкнулись при использовании описанной методики, будет рассказано в следующем разделе.
IDE плагины PVS-Studio автоматически создают suppress файлы при первоначальной разметке сообщений, и в дальнейшем сопоставляют все вновь сгенерированные диагностики с теми, что содержатся в suppress базах. И, если после перепроверки вновь сгенерированное сообщение будет идентифицировано в базе, оно не будет показано пользователю.
После реализации первого работоспособного прототипа нового механизма мы, естественно, захотели посмотреть, как этот механизм покажет себя при работе с реальными проектами. Мы не стали ждать несколько месяцев\лет, пока в таких проектах накопятся достаточное количество изменений, а просто взяли несколько прошлых ревизий в нескольких крупных open source проектах.
Что мы хотели увидеть? Мы брали какую-то достаточно старую ревизию проекта (в зависимости от активности разработчиков, это могла быть и неделя, и целый год), проверяли её нашим анализатором, закладывали все полученные сообщений в suppress базы. Затем обновляли проект до его последней head ревизии и проверяли анализатором снова. В идеале мы должны были бы увидеть сообщений, найденные только на "новом" коде, т.е. коде, который был написан в рассматриваемый нами промежуток времени.
При проверке первого же проекта мы столкнулись с рядом проблем и ограничений нашей методики. Рассмотрим их поподробнее.
Во-первых, что, в принципе, было ожидаемо, сообщения "появлялись вновь" в случае, если модифицировался код в месте выдачи самого сообщения, либо на предыдущей\следующей строке. При этом, если модификация строки самого сообщения вполне ожидаемо приводила к "воскрешению" такого сообщения, то модификация окружающих строк, как может показаться, к этому приводить не должна. Это, в частности, и является одним из основных ограничений выбранной нами методики – мы привязываемся к тексту исходного файла на этих 3-х строчках. Далее, привязываться только к одной строке кажется нецелесообразным – слишком много сообщений потенциально могут быть "перепутаны". В статистике по проектам, которая будет приведена далее, мы обозначим такие сообщения "парными" – т.е. сообщения, которые как бы уже есть в suppress базах, но всплыли вновь.
Во-вторых, выяснилась ещё одна особенность (а точнее – очередное ограничение) нашего нового механизма – "воскрешение" сообщений в h (заголовочных) файлах в случае, когда эти файлы включались в другие исходные файлы, в других проектах. Это ограничение связано с тем, что базы генерируются на уровне IDE проекта. Аналогичная ситуация возникает и в случае появления новых проектов в решении, переиспользующих заголовочные\исходные файлы.
Далее, оказалось не очень хорошей идеей ориентироваться на текст сообщения анализатора для идентификации такого сообщения в базах. Иногда текст сообщения анализатора может содержать в себе номера строк в исходном коде (они меняются в случае сдвига) и имена переменных, фигурирующих в коде пользователя. Мы решили данную проблему, сохраняя в базе не полное сообщение анализатора – из него вырезаются все цифровые символы. А вот "воскрешение" сообщения при изменении имени переменной мы решили посчитать корректным – ведь могло поменяться не только её имя, но и определение – мы считаем это уже "новым" кодом.
Наконец, некоторые сообщения "мигрировали" – то ли код с ними был скопирован в другие файлы, то ли файлы включались в другие проекты, что, в принципе, пересекается с самой первой описанной проблемой нашей методики.
Перечислим статистику по нескольким проектам, на которых мы тестировали новую систему. Большое количество диагностических сообщений вызвано тем, что учитывались вообще все сообщения. В том числе диагностика 64-битных ошибок, которая к сожалению, вообще генерирует много ложных срабатываний и с этим ничего нельзя поделать.
Какие же выводы мы можем сделать из полученных нами результатов?
Вполне ожидаемо, что на проектах, не развивающихся активно, мы не увидели большого числа "новых" сообщений, даже на таком большом отрезке времени, как целый год. Заметим, что для таких проектов мы не стали считать количество "парных" и мигрировавших сообщений.
Но наибольший интерес, безусловно, для нас представляют "живые проекты". В частности, на примере LLVM мы видим, что количество "новых" сообщений составило 34% от помеченных на версии, отстающей по времени всего на 1.5 месяца! Тем не менее, из этих 18 000 новых сообщений только 1000 (500 мигрировавших + 500 парных) относятся к ограничениям нашей методики, т.е. всего 5% от общего числа новых сообщений.
На наш взгляд, эти цифры очень хорошо продемонстрировали жизнеспособность нового механизма. Конечно, стоит помнить, что новый механизм подавления ни в коем случае не является "панацеей", но ничто не отменяет возможность использовать многочисленные уже существующие ранее методы подавления\фильтрации. Например, если какое-то сообщение в h файле начинает очень часто "всплывать", не будет ничего плохого в том, чтобы "убить" его навсегда, добавив к строке комментарий вида //-Vxxx.
Несмотря на то, что новый механизм уже достаточно отлажен, и мы готовы его показать нашим пользователям в следующем релизе, мы решили продолжать его тестировать, организовав регулярную (каждую ночь) проверку проекта LLVM/Clang. Новый механизм позволит нам смотреть только на сообщения из "свежего" кода проекта – теоретически мы сможем находить ошибки ещё до того, как их обнаружат у себя разработчики. Это очень хорошо покажет реальную пользу от регулярного использования статического анализа – и это было бы невозможно без нашей новой системы подавления, ведь нереально просматривать по 50 000 каждый день. Ждите отчётов о найденных свежих багах в Clang в нашем твиттере.