Хабрахабр

Короткая заметочка про PVS Studio в CI (и чего не хватает)

На хабре уже немало статей ей посвящённых, но я хочу коснуться ещё одного аспекта — использование данного инструмента в системе непрерывной интеграции. Я думаю, нет смысла в очередной раз рекламировать замечательный инструмент для статического анализа — PVS Studio.

В силу используемых инструментов сборка ведётся для проектов, созданных на C# (msbuild) и C++ (msbuild, CMake). Итак, есть некоторая организация, есть в ней CI, который работает просто: Jenkins получает хук после push-а в Git, после чего запускает некоторый пайплайн. На одном из финишных этапов запускается генерация отчётов в том числе с помощью PVS Studio (среди прочего — cppcheck, но это не важно для дальнейшего повествования).

PVS Studio имеет консольный инструмент анализа, который запускается из командной строки: PVS-Studio_Cmd.exe --target "$" --output report.plog --progress

На входе — имя проекта (.sln), на выходе — отчёт.

Схема документа встроена, поэтому никаких неожиданностей по выходному формату быть не может. Отчёт — файл с расширением .plog, представляет собой обычный XML-файл. ПО крайней мере пока разработчики схему не поменяют, но не будем рассматривать этот вариант.

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

Но читать XML глазами — удовольствие так себе, поэтому нужен какой-то способ просмотра и навигации.

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

А есть достаточно стандартный путь, который позволяет преобразовать XML во что-то другое: XSLT. Поэтому пойдём другим путём и посмотрим, что можно сделать. Сейчас, наверное, кого-то из читателей передёрнуло, но, тем не менее, предлагаю продолжить чтение.

Он просто сопоставляет входному шаблону правило преобразования, но Мы для себя сделали преобразование в HTML-отчёт. XSLT — это язык преобразования XML-документов во что-то другое.

Каждая запись в отчёте будет соответствовать строке таблицы со следующими столбцами: Надеюсь, никто не будет судить меня за вёрстку таблицами, ибо данные по своей природе табличные.

  1. Номер строки в таблице.
  2. Имя файла.
  3. Номер строки.
  4. Код ошибки.
  5. Сообщение об ошибке.

Номер строки просто удобно для устной ссылки при обсуждении.

Но об этом чуть позже. Имя файла совместно с именем строки позволяют создать ссылку на репозиторий.

Код ошибки обрамляется ссылкой на сайт разработчиков PVS-Studio: http://viva64.com/en/{ErrorCode} (или /ru/, кому как нравится).

Сообщение об ошибке — без комментариев.

Есть некоторые моменты, с которыми пришлось иметь дело.

Первая задача решается с помощью выражения xsl:sort, вторая — count(тег[условие]). Во-первых, хотелось бы, чтобы сообщения сортировались по уровню важности, а также иметь общее количество сообщений каждого типа.

Надо просто отрезать префикс, соответствующий имени каталога с проектом, в который клонировался репозиторий (у нас Git, но это легко адаптируется). Второе: имя файла указывается полным, а для формирования ссылки на систему контроля версий нужно относительное имя. Дальше относительно просто: удалить из строки с именем файла общий префикс с именем каталога, куда репозиторий склонирован. Но для того, чтобы этот путь появился, нам надо параметризовать XSL-трансформацию с помощью конструкции xsl:param. Надо сказать, в XSLT эта задача решается достаточно изощрённо.

Решается с помощью параметра с идентификатором коммита. Третье: проверка относится к конкретной ревизии в репозитории, и это тоже надо иметь в виду. Решается задача следующим образом: все внешние проекты складываем в некоторый каталог, имя которого не содержится в нашем проекте. Аналогично для веток.
Четвёртое: если используются сторонние библиотеки с исходниками, не стоит смешивать предупреждения в них с предупреждениями в нашем проекте. Для большей гибкости можно параметризовать трансформацию и назначить этому каталогу имя по умолчанию: <xsl:param name="external" select="'External'" /> Теперь, если имя файла содержит этот подкаталог (на самом деле просто подстроку), то запись в plog-е не попадает в отчёт, но считается как "скрытая" в заголовке отчёта.

Мы используем redmine+gitolite. Ну и ещё одна маленькая задачка: собрать ссылку на репозиторий. Опять-таки, адаптируемо.

Поскольку многие параметры являются константыми для преобразования, можно подготовить константный префикс URL-а:

<xsl:variable name="repo"> <xsl:text>http://redmine.your-site.com/projects/</xsl:text> <xsl:value-of select="$project" /> <xsl:text>/revisions/</xsl:text> <xsl:value-of select="$revision" /> <xsl:text>/entry/</xsl:text> </xsl:variable>

CSS внедряем в страницу, просто чтобы иметь отчёт одним файлом. Немного красивостей со стилизацией, и можно пользоваться. Картинки нам тоже не нужны.

Полный код трансформации под спойлером

XSLT

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns="http://www.w3.org/1999/xhtml" exclude-result-prefixes="msxsl"
> <xsl:output method="html" indent="yes"/> <xsl:param name="project" /> <xsl:param name="base-path" /> <xsl:param name="branch" select="'master'" /> <xsl:param name="revision" select="'[required]'" /> <xsl:param name="external" select="'External'" /> <xsl:variable name="repo"> <xsl:text>http://redmine.your-company.com/projects/</xsl:text> <!-- # !!!attention!!! # --> <xsl:value-of select="$project" /> <xsl:text>/revisions/</xsl:text> <xsl:value-of select="$revision" /> <xsl:text>/entry/</xsl:text> </xsl:variable> <xsl:template name="min-len"> <xsl:param name="a" /> <xsl:param name="b" /> <xsl:variable name="la" select="string-length($a)" /> <xsl:variable name="lb" select="string-length($b)" /> <xsl:choose> <xsl:when test="$la &lt; $lb"> <xsl:value-of select="$la"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="$lb" /> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template name="strdiff-impl"> <xsl:param name="mask" /> <xsl:param name="value" /> <xsl:param name="n" /> <xsl:param name="lim" /> <xsl:choose> <xsl:when test="$n = $lim"> <xsl:value-of select="substring($value, $lim + 1)" /> </xsl:when> <xsl:when test="substring($mask, 0, $n) = substring($value,0, $n)"> <xsl:call-template name="strdiff-impl"> <xsl:with-param name="lim" select="$lim" /> <xsl:with-param name="mask" select="$mask" /> <xsl:with-param name="value" select="$value" /> <xsl:with-param name="n" select="$n + 1" /> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="substring($value, $n - 1)"/> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template name="strdiff"> <xsl:param name="mask" /> <xsl:param name="value" /> <xsl:choose> <xsl:when test="not($value)" /> <xsl:when test="not($mask)"> <xsl:value-of select="$value" /> </xsl:when> <xsl:otherwise> <xsl:call-template name="strdiff-impl"> <xsl:with-param name="mask" select="$mask" /> <xsl:with-param name="value" select="$value" /> <xsl:with-param name="lim"> <xsl:call-template name="min-len"> <xsl:with-param name="a" select="$mask" /> <xsl:with-param name="b" select="$value" /> </xsl:call-template> </xsl:with-param> <xsl:with-param name="n" select="1" /> </xsl:call-template> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="/*"> <xsl:variable select="Solution_Path/SolutionPath" name="solution" /> <xsl:variable select="PVS-Studio_Analysis_Log [not(contains(File, $external))] [ErrorCode!='Renew'] " name="input" /> <html lang="en"> <head> <style type="text/css"> <![CDATA[ #report * {font-family: consolas, monospace, sans-serif; } #report {border-collapse: collapse; border: solid silver 1px;} #report th, #report td {padding: 6px 8px; border: solid silver 1px;} .sev-1 {background-color: #9A2617;} .sev-2 {background-color: #C2571A;} .sev-3 {background-color: #BCA136;} .sev-hidden {background-color: #999; } #report tbody * {color: white;} .fa * { color: #AAA; } a {color: #006;} .stat {padding: 20px;} .stat * {color: white; } .stat span {padding: 8px 16px; } html {background-color: #EEE;} .success {color: #3A3; } ]]> </style> </head> <body> <h1>PVS-Studio report</h1> <h2> <xsl:call-template name="strdiff"> <xsl:with-param name="value"> <xsl:value-of select="$solution" /> </xsl:with-param> <xsl:with-param name="mask"> <xsl:value-of select="$base-path" /> </xsl:with-param> </xsl:call-template> </h2> <div class="stat"> <span class="sev-1"> High: <b> <xsl:value-of select="count($input[Level=1])" /> </b> </span> <span class="sev-2"> Meduim: <b> <xsl:value-of select="count($input[Level=2])" /> </b> </span> <span class="sev-3"> Low: <b> <xsl:value-of select="count($input[Level=3])" /> </b> </span> <span class="sev-hidden" title="Externals etc"> Hidden: <b> <xsl:value-of select="count(PVS-Studio_Analysis_Log) - count($input)"/> </b> </span> </div> <xsl:choose> <xsl:when test="count($input) = 0"> <h2 class="success">No error messages.</h2> </xsl:when> <xsl:otherwise> <table id="report"> <thead> <tr> <th> # </th> <th> File </th> <th> Line </th> <th> Code </th> <th> Message </th> </tr> </thead> <tbody> <xsl:for-each select="$input"> <xsl:sort select="Level" data-type="number"/> <xsl:sort select="DefaultOrder" /> <tr> <xsl:attribute name="class"> <xsl:text>sev-</xsl:text> <xsl:value-of select="Level" /> <xsl:if test="FalseAlarm = 'true'"> <xsl:text xml:space="preserve"> fa</xsl:text> </xsl:if> </xsl:attribute> <th> <xsl:value-of select="position()" /> </th> <td> <xsl:variable name="file"> <xsl:call-template name="strdiff"> <xsl:with-param name="value" select="File" /> <xsl:with-param name="mask" select="$base-path" /> </xsl:call-template> </xsl:variable> <a href="{$repo}{translate($file, '\', '/')}#L{Line}"> <xsl:value-of select="$file" /> </a> </td> <td> <xsl:value-of select="Line"/> </td> <td> <a href="http://viva64.com/en/{ErrorCode}" target="_blank"> <xsl:value-of select="ErrorCode" /> </a> </td> <td> <xsl:value-of select="Message" /> </td> </tr> </xsl:for-each> </tbody> </table> </xsl:otherwise> </xsl:choose> </body> </html> </xsl:template> <xsl:template match="@* | node()"> <xsl:copy> <xsl:apply-templates select="@* | node()"/> </xsl:copy> </xsl:template>
</xsl:stylesheet>

Мы запускаем трансформацию с помощью маленькой консольной утилиты, написанной на C#, но вы можете сделать это и по-другому (если надо — тоже поделюсь, там нет ничего сложного и секретного).
А дальше из этого можно сделать dashboard, но это уже, как говорится, совсем другая история.

Есть один не то баг, не то фича, которая делает невозможным полноценно сделать то, что описано выше, притом это касается только С++-проектов, в C# такой беды нет. А теперь немного плача в сторону разработчиков. А когда redmine (и прочий веб) хостится на UNIX-подобных системах с регистрозависимыми именами файлов, ломается регистр при формировании ссылок на файлы, что делает ссылки неработоспособными. Когда формируется plog-файл, в теге <File> имя всегда приводится к нижнему регистру. Такая вот печаль.

На письмо в техподдержку я получил ответ, что такое поведение обусловлено внешним API, но непонятно, почему оно такое избирательное и касается только C++, и не касается C#.
Поэтому я пока, как и обещал, взываю к продолжению Paull и надеюсь на плодотворное сотрудничество.

Спасибо за внимание.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»