スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

detekt × Dangerで、プルリクコメントにルール名を表示する

こんにちは。スタディサプリ Androidエンジニアの@morayl です。

本記事では、Kotlinの静的解析ツールであるdetektの解析結果をDangerでプルリクにコメントする際に、ルール名も一緒にコメントするためにしたことを紹介します。
Dangerの基礎言語であるRubyは初心者なので、有識者から学びながらトライしました。

背景と結果

私が所属するチームでは最近、detektを導入し、Dangerを使ってプルリク上にコメントが出るようにしました。

この状態では、detektの指摘コメントだけが出ています。

一見問題無さそうですが、detektの指摘はルールで管理されているため、ルール名があったほうが便利です。

ルール名が分かることは、下記の点で重要です。

  • 修正する時に困る
    • 指摘の詳細やどのように直したほうが良いかなどは、detektのルールガイドに載っていますが、メッセージから何のルールに引っかかっているのかは調べづらい
  • 設定値(しきい値など)を調整したり無効にしたりする場合、設定ファイルはルール名で管理されているので、探す必要がある
  • 理由があって指摘を個別に抑制したい場合にルール名が必要になる。(Kotlinでは、メソッドや変数に@Suppress("ルール名")と書くことで抑制できる。)

メッセージからルールを推察することも出来ますが、detektをある程度経験しなければ難しく、よく使う人でもルール名がちゃんと分かるかというと難しいです。

そこで、ルールも一緒にコメントされるようにしました。

このコメントの場合、「MagicNumberというルールの指摘」ということがすぐに分かります。

前提条件

detektの結果をDangerでコメントする際には、danger-checkstyle_format を使っています。

確認したライブラリのバージョンは下記です。

  • detekt:1.23.4
  • Danger:9.4.2
  • danger-checkstyle_format:0.1.1

結論

DangerFileのdetektの処理の前に、下記を加えるだけです。

# 加筆
class ::CheckstyleError
  alias_method :original_message, :message

  def message
    "`#{source}`\n\n#{original_message}"
  end
end

# 元々あるdetektの処理
checkstyle_format.base_path = Dir.pwd
Dir.glob("check/reports/detekt/**/*.xml").each do |file|
  checkstyle_format.report file
end

道筋と解説

ルールはどこにあるのか

まず、そもそもルールの情報があるのかを調べます。
checkstyle_format.reportに設定するファイルはxmlです。ローカルでも出力できるので、detektをローカルで走らせます。
すると、出力されたxmlの中には、<errorの中のsourceにあることが分かりました。

<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="...HogeUtils.kt">
    <error line="7" column="17" severity="warning" message="This expression contains a magic number. Consider defining it to a well named constant." source="detekt.MagicNumber" />
</file>
</checkstyle>

また、プルリクに出ているメッセージはmessageに記載されているものだということも分かります。

コメントはどのように作られるか

次に、コメント生成部分を探します。
使っているdanger-checkstyle_formatライブラリを確認します。
その生成部分にsourceを入れることが出来れば、目的が達成できるからです。
CheckstyleErrorという型には、 sourceが定義してあります。(参考)

CheckstyleError.new(
  parent_node[:name].sub(/^#{base_path}/, ""),
  node[:line].to_i,
  node[:column].nil? ? nil : node[:column].to_i,
  node[:severity],
  node[:message],
  node[:source]
)

次に、Dangerでコメントする部分を確認すると、ここではsourceは使われていません。(参考)

warn(error.message, file: error.file_name, line: error.line)

コメントをどうカスタマイズするか

やりたいことは「warnの第一引数のerror.messageに、sourceも含めて表示したい」です。
やり方はいくつかありますが、今回はRubyのモンキーパッチというものを使いました。

これはすでに定義されたクラスの動きを変えるもので、無闇に使うべきものではありません。
正攻法としては、「ライブラリのPRを出す」「forkして使う」「ライブラリを使わず自前で書く」などがあります。
今回は、プロダクトに影響がない部分であること・修正範囲が小さくサクッと試したかったことから、モンキーパッチを試してみることにしました。

Dangerfileが実行されるときには、すでにライブラリが読み込まれ、danger-checkstyle_formatが使えるようになっています。
そこで、下記Dangerのメソッド実行前に、CheckstyleErrormessageを書き換えて、sourceも表示することにしました。

warn(error.message, file: error.file_name, line: error.line)

そして、これがモンキーパッチです。

class ::CheckstyleError
  alias_method :original_message, :message

  def message
    "`#{source}`\n\n#{original_message}"
  end
end
class ::CheckstyleError

すでにある同じ名前のクラスの定義を書くことで、動きを上書きすることが出来ます。

  alias_method :original_message, :message

alias_methodを使うと、下記の形で元あるメソッドを新しい呼び名で複製することが出来ます。

alias_method 新しいメソッド名, すでにあるメソッド名

ここでは、original_messageをというメソッドを作成し、messageと同じ動きをするようにしています。

そして最後に、元あるmessageを上書きしています。

  def message
    "`#{source}`\n\n#{original_message}"
  end

alias_methodで作られたoriginal_messageは、その時点でのmessageの動きをするため、上書きされたmessageが使われて循環参照になることはありません。 これで、以降でmessageが使われた場合は、sourceも含めた文字列を表示するように出来ました。

最後に

今回は、Dangerでコメントするdetektの指摘内容にルール名を含めることに方法について紹介しました。 今までRubyを使う機会はほとんどありませんでしたが、今回のことを通じてRubyと少しだけ仲良くなれた気がします。
今回はモンキーパッチを使いましたが、コメントする機能はDangerにあり、checkstylexmlが解析できれば、ライブラリを使わず出力内容を自分で自由に決めることも出来ます。
更にやりたいこと出てきたら、今度は自前での実装に挑戦してみようと思いました。