静的解析結果をGitHubのPRやGitLabのMRにコメントするViolationsツールの紹介

静的解析ツールを導入しているが、事前に解析結果をチェックしてといってもなかなか潰しきれない。

GitサーバーとしてGitLabを使っているので、マージリクエスト(GitHubなどのプルリクエストに相当、以下MR)でコードレビューを行っているが、静的解析結果をMRのコメントとして連携できれば、コードレビューを依頼する前にある程度取り切れるかと思い、調査したところ、うまいことやってくれるツールがあったのでメモ。

環境

Gradle v6.6.1、GitLab CE v13.6.1。

ツールの概要

「Violations」および「Violation Comments」というツールがあった。

github.com

上記リンクの「Violations Lib」というJavaライブラリがあり、そこからGitHubやBitbucketのプルリクエスト、およびGitLabのマージリクエストへのコメント連携用に「Violation Comments to ~ Lib」がサポートされている。

さらに、「Violation Comments」を実行するためのコマンドラインツールの「Violation Comments to ~ Command Line」や、Maven/Gradle/Jenkinsプラグインの「Violation Comments to ~ Plugin」が用意されている。

利用可能な静的解析ツールの詳細は、上記リンク先参照。

JavaやGroovy、Kotlin、ScalaといったJVM言語だけでなく、C++、Go、JavaScript、TypeScript、PHPPythonRuby、またAnsibleLintやCloudFormation Linterなどにも対応している。

使用例

今回は、Javaプロジェクトに対し、GitLab CIからGradleプラグインで、SpotBugs、PMD/CPD、Checkstyleの解析結果を連携してみる。

build.gradle

プラグインの追加

プラグイン追加方法は以下の通り。

記述は以下のようになる。PMDとCheckstyleは、コアプラグインなのでバージョン指定不要っぽい。

// plugins DSLの場合
plugins {
  id 'com.github.spotbugs' version '4.7.2'
  id 'pmd'
  id 'de.aaschmid.cpd' version '3.2'
  id 'checkstyle'
  id 'se.bjurr.violations.violation-comments-to-gitlab-gradle-plugin' version '1.41.3'
}

// legacy plugin applicationの場合
buildscript {
  repositories {
    maven {
      url 'https://plugins.gradle.org/m2/'
    }
  }

  dependencies {
    classpath 'gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.2'
    classpath 'se.bjurr.violations:violation-comments-to-gitlab-gradle-plugin:1.41.3'
    classpath 'de.aaschmid:gradle-cpd-plugin:3.2'
  }
}

apply plugin: 'com.github.spotbugs'
apply plugin: 'pmd'
apply plugin: 'de.aaschmid.cpd'
apply plugin: 'checkstyle'
apply plugin: 'se.bjurr.violations.violation-comments-to-gitlab-gradle-plugin'
静的解析ツールプラグインの設定

プラグインの設定方法を元に、設定していく。

  • SpotBugs
    • 2021/8/15時点で、READMEのConfigure SpotBugs Pluginが古いっぽい。値の型が org.gradle.api.provider.Property になっているので、代入ではなく set メソッドを使わないと警告が出る模様。
  • PMD
  • CPD
  • Checkstyle

記述は以下のようになる。

spotbugs {
  toolVersion.set('4.3.0')
  ignoreFailures.set(true)
  showStackTraces.set(false)
}

pmd {
  toolVersion = '6.36.0'
  ignoreFailures = true
  consoleOutput = false

  // ルールセットはとりあえず全部入り
  ruleSets = [
      'category/java/bestpractices.xml',
      'category/java/codestyle.xml',
      'category/java/design.xml',
      'category/java/documentation.xml',
      'category/java/errorprone.xml',
      'category/java/multithreading.xml',
      'category/java/performance.xml',
      'category/java/security.xml',
  ]
}

cpd {
  // 解析結果ファイルの文字コード。未設定だとOSのデフォルトエンコーディングになるため、WindowsでCP932になるのを抑制するために指定
  encoding = 'UTF-8'
  language = 'java'
  ignoreFailures = true
  ignoreLiterals = true // リテラル値の違いを無視
  ignoreIdentifiers = true // 変数名の違いを無視
  ignoreAnnotations = true // アノテーションを無視
}

checkstyle {
  toolVersion = '8.45.1'
  configFile = file('config/checkstyle/google_checks.xml')
  ignoreFailures = true
  showViolations = false
}

// 解析時間の短縮のため、ユニットテストに対しては解析対象から除外する
spotbugsTest.enabled = false
pmdTest.enabled = false
cpdCheck { source = sourceSets.main.allJava }
checkstyleTest.enabled = false
Violation Commentsプラグインの設定

GradleプラグインページからはGitLab Gradle Pluginへリンクしているが、設定できるパラメーターの詳細はGitLab Command Lineを見たほうがいい。

記述は以下のようになる。type: import se.bjurr.violations.comments.gitlab.plugin.gradle.ViolationCommentsToGitLabTask のタスクを宣言し、設定していく。

import se.bjurr.violations.comments.gitlab.plugin.gradle.ViolationCommentsToGitLabTask

task violationCommentsToGitLab(type: ViolationCommentsToGitLabTask) {
  // GitLabのURL、プロジェクトID、MRのインターナルID、コメント用ユーザーのAPIトークンはCIから引数で渡す
  gitLabUrl = project.findProperty("gitLabUrl")
  projectId = project.findProperty("projectId")
  mergeRequestIid = project.findProperty("mergeRequestIid")
  apiToken = project.findProperty("gitLabApiToken")

  apiTokenPrivate = true
  createCommentWithAllSingleFileComments = false // 問題をまとめたコメントをしない
  createSingleFileComments = true // ディスカッションとしてコメントする
  minSeverity = "INFO" // コメントする最小の重大度

  // 解析ツールの設定を配列で指定する
  // 1. READMEで指定された解析ツールの種類
  // 2. 解析結果の検索ディレクトリ
  // 3. 解析結果ファイル検索用正規表現
  // 4. コメントに表示する解析ツール名(レポーター)
  // ディレクトリは絶対パスで指定しないと、Windows環境ではファイルの検索ができなかった。該当部分のソースがおかしかった気がする。
  // また、ディレクトリ指定とファイル検索の正規表現が、組み合わせによって検索できないことが多々あり、この記述に落ち着いた。
  violations = [
      ['FINDBUGS', file("${buildDir}/reports/spotbugs").absolutePath, '.*/main\\.xml\$', 'Spotbugs'],
      ['PMD', file("${buildDir}/reports/pmd").absolutePath, '.*/main\\.xml\$', 'PMD'],
      ['CPD', file("${buildDir}/reports/cpd").absolutePath, '.*/cpdCheck\\.xml\$', 'CPD'],
      ['CHECKSTYLE', file("${buildDir}/reports/checkstyle").absolutePath, '.*/main\\.xml\$', 'Checkstyle'],
  ]
}
コメント形式のカスタマイズ

前述の設定だと、こちらのデフォルト形式でコメントされる。

PMDで System.out.println があった場合に、実際にコメントされるテキストは以下。

f:id:hepokon365:20210815212749p:plain
デフォルトのコメント

SourceにGradleプロジェクトのgroupやnameが設定されるが、ファイルパスが分かれば十分だし、縦に長いので調整したい。

コメントテンプレートは、 commentTemplate として mustacheテンプレートで記述可能。mustacheの実装としてはMustache.javaが使われる。

前述の task violationCommentsToGitLab 配下に、以下を追加。

task violationCommentsToGitLab(type: ViolationCommentsToGitLabTask) {
  // 中略

  // コメント形式のカスタマイズ、
  commentTemplate = """\
  **重大度**: {{violation.severity}}, **解析ツール**: {{violation.reporter}}{{#violation.rule}}, **ルール**: {{violation.rule}}{{/violation.rule}}

  **対象**: {{changedFile.filename}} \\# {{violation.startLine}}行目{{#violation.endLine}}~{{violation.endLine}}行目{{/violation.endLine}}

  **内容**: {{violation.message}}
  """.stripIndent()
}

すると、コメントされるテキストは以下のようになる。

f:id:hepokon365:20210815214951p:plain
テンプレート変更後のコメント

GitLab設定

コメント用のユーザーを作成し、ユーザー設定 > アクセストークンから、apiスコープのパーソナルアクセストークンを作成しておく。

.gitlab-ci.yml設定

GitLab CI/CD Variablesとして、先ほど作成したユーザーのアクセストークンを保存しておく。名前は VIOLATION_API_TOKEN とした。

マージリクエストに限定されたジョブに対し、以下のscriptを記述すると、MRの変更箇所に対して静的解析結果がコメントされる。

  script:
    - |-
      ./gradlew check violationCommentsToGitLab -x test \
        -PgitLabUrl=${CI_SERVER_URL} \
        -PprojectId=${CI_PROJECT_ID} \
        -PmergeRequestIid=${CI_MERGE_REQUEST_IID} \
        -PgitLabApiToken=${VIOLATION_API_TOKEN}

振り返り

最悪、自前でGitLab APIを叩いて実現しようと思っていたので、ものすごく助かった。

めちゃくちゃ便利だと思ったんだけど、2021/8/15時点で日本語の紹介を見つけられなかった。マイナーなのか、有償の機能とか使ってやっているのかな?