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


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


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


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


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


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


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


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


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


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


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


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

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


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


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


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


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


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


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


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


Ну и ещё одна маленькая задачка: собрать ссылку на репозиторий. Мы используем 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# такой беды нет. Когда формируется plog-файл, в теге <File> имя всегда приводится к нижнему регистру. А когда redmine (и прочий веб) хостится на UNIX-подобных системах с регистрозависимыми именами файлов, ломается регистр при формировании ссылок на файлы, что делает ссылки неработоспособными. Такая вот печаль.


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


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

Комментарии (15)


  1. SvyatoslavMC
    20.09.2018 16:16

    На хабре публиковалась статья: Отчёт PVS-Studio теперь в Html формате. Возможно, мы совместно можем доработать существующие форматы.


    1. a-tk Автор
      20.09.2018 16:22

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


  1. Paull
    20.09.2018 17:11

    Спасибо, что более подробно описали ваш сценарий использования.

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

    API для разбора C++ и C# проектов достаточно существенно различаются, при этом, т.к. C# анализатор разрабатывался позже, он в целом использует более «новые» API. Для C++ мы также переводили значительную часть функционала на новые API, но некоторый legacy-код ещё остаётся + не для всего новые подходы работают, к сожалению.

    Нам потребуется какое-то время, чтобы проверить все места, где регистр может портиться, и понять, насколько эти места остаются критичными (к сожалению, нет просто одного места, где мы «портим» путь). Также, изменение текущего поведения может задеть некоторые другие места, где ожидается как-раз регистро-независимый путь, например, при наложении фильтров на проверяемые файлы или результаты анализа, при запуске анализа на Windows, что потребует дополнительного тестирования.

    Я свяжусь с вами, когда ситуация с этим вопросом станет более понятной.

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

    На самом деле, мы обычно всё-таки не рекомендуем нашим пользователям завязываться на формат xml отчёта ) Дело в том, что этот формат является представлением сериализованного объекта, завязанного на наш UI и в будущем, особенно в случае потенциальных серьёзных переработок UI компонентов, этот формат может поменяться.

    Мы рекомендуем использовать одну из наших утилит для трансформации лога в один из стандартных форматов, таких как csv, html, plain text, и работать уже дальше с ними.


    1. a-tk Автор
      20.09.2018 17:20

      Спасибо за ответ. Ситуация стала более ясной, чем было описано в письме.
      Вот только про XML весьма жаль: очень уж удобно HTML нужного вида делать из XML. Надеюсь, этот формат когда-нибудь появится в списке «официальных» выходных форматов.


      1. Paull
        20.09.2018 17:31

        Вот только про XML весьма жаль: очень уж удобно HTML нужного вида делать из XML. Надеюсь, этот формат когда-нибудь появится в списке «официальных» выходных форматов.

        Возможно в более простом виде — в текущем XML слишком много legacy-«мусора» для обратной совместимости, и лишних деталей. Возможно, это будет Json. Наш linux-конвертер уже умеет сохранять подобный более простой формат, но там может не оказаться всего, что вам нужно, т.к. он делался для несколько других целей + на Windows его может быть неудобно использовать с plog'ом.

        У нас сейчас, в обозримой перспективе, нет планов отказываться или кардинально менять формат лога MSBuild анализатора, поэтому пока-что можно не переживать по этому поводу )


        1. a-tk Автор
          20.09.2018 18:03

          Наш linux-конвертер уже умеет сохранять подобный более простой формат, но там может не оказаться всего, что вам нужно,

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


  1. Imposeren
    20.09.2018 18:47

    А почему не захотели конвертировать во что-то, что можно отображать прямо в Jenkins? Например Warnings Plugin + Analysis Collector Plugin. Там вроде легко парсинг из xml настраивается. И исходники подсветить можно:
    wiki.jenkins.io/display/JENKINS/Static+Code+Analysis+Plug-ins#StaticCodeAnalysisPlug-ins-source

    Плюс можно «глубже» в пайплайн встраивать, например health-check настраивать:

                        warnings([
                            canComputeNew: false,
                            canResolveRelativePaths: false,
                            defaultEncoding: '',
                            excludePattern: '',
                            healthy: '',
                            includePattern: '',
                            messagesPattern: '',
                            parserConfigurations: [
                                [
                                    parserName: 'PyLint',
                                    pattern: 'reports/flake8.report'
                                ],
                                [
                                    parserName: 'JSLint',
                                    pattern: 'reports/jshint.xml'
                                ]
                            ],
                            unHealthy: ''
                        ])
                        step([
                            $class: 'AnalysisPublisher',
                            defaultEncoding: '',
                            failedNewHigh: '0',
                            failedNewLow: '5',
                            failedNewNormal: '0',
                            failedTotalHigh: '0',
                            failedTotalLow: '50',
                            failedTotalNormal: '32',
                            healthy: '0',
                            unHealthy: '200',
                            unstableTotalLow: '40',
                            unstableTotalNormal: '33',
                            useStableBuildAsReference: true
                        ])
    


    1. a-tk Автор
      20.09.2018 21:46

      Потому что не знали. Спасибо, посмотрим.


    1. rustler2000
      20.09.2018 22:31

      А зачем это в дженкинс тянуть когда есть sonarqube который умеет и git blame и на почту уведомления и статистику и навигацию по коду и варнинги компайлеров и сообщения валгринда и т.д. и т.п.?
      Плагин pvs для sq работает замечательно.


      1. Imposeren
        20.09.2018 23:36

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


      1. Vanger
        21.09.2018 07:40

        Резонный вопрос, если есть настроенный и работающий Jenkins, то зачем еще sonarqube?


        1. rustler2000
          21.09.2018 12:36

          Это как минимум сильно не резонный вопрос.


      1. skymorp
        21.09.2018 09:34

        Уточните, пожалуйста, версию SQ (например 7.3 Community).
        И что делать людям, которые пользуются PVS бесплатно?


        1. rustler2000
          21.09.2018 12:44

          У нас 7.0 коммьюнити. pvs стоит не сильно дороже, чем коммерческий cpp плагин от sonarsource.
          SQ CPP коммьюнити плагин работает замечательно.
          Если у вас pvs бесплатно и плагина вам не полагается — мессаги pvs можно втянуть как мессаги компайлера.


        1. rustler2000
          21.09.2018 12:46

          btw мы мессаги pvs комментариями в геррит гоним на изменения +-Х строк.
          а вся картина и изменения в sq лежит.
          и то и то дженкинс запускает.