notebook

都内でWEB系エンジニアやってます。

reviewdogでpre-commitフックをより快適にする

lint-staged + huskyでプロジェクトのpre-commitフックにリンターを設定して…コミット前に指摘を潰しましょうというのはよくあるやり方である

lint-stagedでは差分に対してのみ各種リンターのチェックができる認識でいたが差分があるファイル単位までフィルタリングできるという感じだった

プロジェクトの途中からリンターを導入する場合など、すでに警告対象のコードが紛れてしまっている場合、そういうファイルを変更するとリンターに引っかかるので同時に直さないと行けないというような感じになる

できれば差分のみに対して警告を出すようにしたい

一方、CIではよくreviewdogを使って差分のみに対してリンターを掛けコメントしている例をよく見る

reviewdog/reviewdog: 🐶 Automated code review tool integrated with any code analysis tools regardless of programming language

github.com

reviewdogのREADMEより

reviewdogはGitHubなどのコードホスティングサービスにレビューコメントを自動的に投稿するためのツールです.レビューするパッチのdiffに所見がある場合、lintツールの出力を使ってコメントとして投稿します。
reviewdogはローカル環境での実行もサポートしており、lintツールの出力をdiffでフィルタリングすることができます。

CIだけじゃなくローカルでも実行ができるということはpre-commitフックでも実行ができる

reviewdogを使えば変更のあったファイルだけじゃなく差分に対してリンターで警告を出すことができるな

ということでいくつか試してみた

以降の例ではhuskyやlint-staged,reviewdogを使っている前提で話を進める、インストール方法や基本的な使い方については他にも紹介している記事がたくさんあるので今回は割愛する

lint-stagedの設定

  • lint-staged.config.js(一部)
"*.@(js|ts|tsx)": filenames => `yarn eslint -f compact -c .eslintrc.json ${filenames.join(" ")} | reviewdog -f=eslint-compact -fail-on-error -diff='git diff --cached' -reporter=local`,

eslintを実行してその結果をreviewdogに流している

reviewdogのコマンド部分とオプション

reviewdog -f=eslint-compact -fail-on-error -diff='git diff --cached' -reporter=local

ローカルでの実行

-reporterオプション

  • ローカルに結果を出力する(GitHubやGitLabなどに投稿しない)
  • -reporter localと指定するだけ

差分の指定

-diffオプション

対象とする差分を指定する

CIの場合は、targetブランチと機能ブランチの差分を指定することが多いはず

pre-commitの場合

-diff="git diff --cached"

このようにステージされた差分を指定すれば良い

終了コードのコントロール

-fail-on-errorオプション

リンターから何か出力を受け取った場合、終了コードを1にする

lint-stagedは各チェックでコマンドの終了コードをみて成功、失敗の判断しているためreviewdogの終了コードを見てチェックの成否を判断する

フォーマット

-fオプション

リンター出力のフォーマット指定

さまざまなリンターツールをサポートしている

$ reviewdog -list
rdjson          Reviewdog Diagnostic JSON Format (JSON of DiagnosticResult message)                                             - https://github.com/reviewdog/reviewdog
rdjsonl         Reviewdog Diagnostic JSONL Format (JSONL of Diagnostic message)                                                 - https://github.com/reviewdog/reviewdog
diff            Unified Diff Format                                                                                             - https://en.wikipedia.org/wiki/Diff#Unified_format
checkstyle      checkstyle XML format                                                                                           - http://checkstyle.sourceforge.net/
ansible-lint    (ansible-lint -p playbook.yml) Checks playbooks for practices and behaviour that could potentially be improved  - https://github.com/ansible/ansible-lint
black           A uncompromising Python code formatter                                                                          - https://github.com/psf/black
brakeman        (brakeman --quiet --format tabs) A static analysis security vulnerability scanner for Ruby on Rails applications- https://github.com/presidentbeef/brakeman
cargo-check     (cargo check -q --message-format=short) Check a local package and all of its dependencies for errors            - https://github.com/rust-lang/cargo
clippy          (cargo clippy -q --message-format=short) A bunch of lints to catch common mistakes and improve your Rust code   - https://github.com/rust-lang/rust-clippy
dotenv-linter   Lightning-fast linter for .env files. Written in Rust                                                           - https://github.com/dotenv-linter/dotenv-linter
dotnet          (dotnet build -clp:NoSummary -p:GenerateFullPaths=true --no-incremental --nologo -v q) .NET Core CLI            - https://docs.microsoft.com/en-us/dotnet/core/tools/
eslint          (eslint [-f stylish]) A fully pluggable tool for identifying and reporting on patterns in JavaScript            - https://github.com/eslint/eslint
eslint-compact  (eslint -f compact) A fully pluggable tool for identifying and reporting on patterns in JavaScript              - https://github.com/eslint/eslint
.....
.....
.....

余談だが、reviewdog用のフォーマットrdjson, rdjsonlというのもあるらしい

これは各リンター用のActionsなどで使っているよう

reviewdog/action-eslint: Run eslint with reviewdog

github.com

独自フォーマットの指定

冒頭の設定には出てきていないが紹介しておく

-efmオプション

-fで対応していない出力フォーマットの場合、自身で指定が可能

項目についてはREADMEを読めば分かる

reviewdog/reviewdog: 🐶 Automated code review tool integrated with any code analysis tools regardless of programming language

github.com

たとえば次のフォーマットを指定した場合

-efm="%f:%l:%c %m"

ファイル名:行数:列数 エラーメッセージ

このような形式の出力がでるようにリンター側で調整すればreviewdogが読み込んでくれる

独自リンターとかでも対応可能

めちゃくちゃ親切でerrorformatのplaygroundもあるので実際の出力を調整しながらフォーマット指定を決められる

Errorformat Playground

reviewdog.github.io

lint-stagedの--shellオプション

ここまでreviewdogのオプションの話をしていたが、そもそもreviewdogとリンターを組み合わせる場合はlint-stagedに--shellオプションを付ける必要がある

が、下記の理由で非推奨らしい…

sindresorhus/execa: Process execution for humans

  • クロスplatformではないため、シェルの構文依存になってしまう
  • シェルの評価をするので遅くなる
  • コマンドインジェクションの可能性がある

使う場合は留意したほうがよい

今回はこのオプションありきでなので設定して進めた

各種リンターでの設定

いくつか設定例を載せておく

secretlint

secretlint、reviewdogとも、checkstyleフォーマットに対応しているので指定するだけ

'*': (filenames) => `${__dirname}/node_modules/.bin/secretlint --format checkstyle --secretlintrc ${__dirname}/.secretlintrc.json ${filenames.join(' ')} | reviewdog -fail-on-error -f checkstyle -diff "git diff --cached" -reporter local`

actionlint

 '.github/workflows/*.{yml,yaml}': (filenames) => `actionlint -format '{{range $err := .}}{{$err.Filepath}}:{{$err.Line}}:{{$err.Column}} {{$err.Kind}} {{$err.Message}}\n{{$err.Snippet}}\n\n{{end}}' ${filenames.join(' ')} | reviewdog -fail-on-error -efm="%f:%l:%c %m" -diff='git diff --cached' -reporter local`,

actionlintはcheckstyleに対応してなさそうだったので、reviewdogが解釈できるフォーマットに指定した

actionlint/usage.md at main · rhysd/actionlint · GitHub

grep

単純なgrepも書ける

'*.@(js|ts|tsx)': (filenames) => `grep -nH console.log ${filenames.join(' ')} | reviewdog -fail-on-error -efm="%f:%l: %m" -diff='git diff --cached' -reporter local`,

例でのconsole.logはeslintでも対応できるが、他にも独自にこの文字列は入れたくないといったパターンで活用できる

まとめ

  • lint-staged + huskyでのpre-commitフックにreviewdogを加えて変更差分に対してのみのリンターチェックが可能になった
  • eslint,actionlint,secretlint、特定文字列をgrepして検出する設定例を紹介した

プロジェクトに入れる場合、これのために開発環境へ各種コマンド入れておくの?という話になるかなと思うので説明だったり合意が必要だと思う

さっとできる範囲だとglobalなGit hookに対して設定しておくといろいろとはかどるはず

自分は下記のリポジトリを参考にlocalとglobal両方のhookを実行できるようにしている

azu/git-hooks: @azu's global git hooks

github.com

いくつか自分用にカスタマイズしてたりするので別途記事にできれば

  • 自分用のglobalなgit-hooks

swfz/git-hooks: global git hooks

github.com