🐕

コードの複雑度をあげる Pull Requests を GitHub Actions で止めよう

2022/10/12に公開
3

循環的複雑度が閾値を超えた Pull Requests に、自動的に変更をリクエストする

「コードの品質を、維持したいよーーー」

ということで、テストや Linter を GitHub Actions で実行している環境はよくあると思いますが、今回は 循環的複雑度 を継続的に計測して、閾値を超えた場合に自動的に Pull Request に対して Request Changes のレビューをしようという試みです。

Lizard

この例では、Lizard を使用して CCN を計測します。
おそらく似たようなツールでも同様に実行することができると思います。

Lizard は Python で開発されている CCN 計測ツールです。(追記:シンプルに書いてしまいましたが、もちろん他の指標も計れます)
以下のようにサポート言語が多いので、大抵の場合で採用できそうです。

サポート言語 (1.17.10)

  • C/C++ (works with C++14)
  • Java
  • C# (C Sharp)
  • JavaScript (With ES6 and JSX)
  • TypeScript
  • Objective-C
  • Swift
  • Python
  • Ruby
  • TTCN-3
  • PHP
  • Scala
  • GDScript
  • Golang
  • Lua
  • Rust
  • Fortran
  • Kotlin

使用例

以下のようにすると、とある PHP プロジェクトの ./src ディレクトリ以下の計測レポートを HTML で書き出すことができます。

lizard -l php -H -o ccn_report.html ./src

# 手元に Python なんぞないわって時は
# docker run -v $PWD:/lizard --rm srzzumix/lizard -l php ...

こんなレポートが出力できます。(試しに WordPress を食わせてみました)

GitHub Actions の設定

実行ができれば、あとは GitHub Actions の Workflow を書いてあげるだけです。
lizard コマンドは Warning があると終了ステータスが設定されるので、if: ${{ failure() }} で判定ができます。

コメントを整形してあげると少し見やすくしたりもできるかなとは思いますが、こんなメッセージ見ないで済むコードを書きたいので(決して面倒なわけでは……!)以下が最小限のサンプルになるかなと思います。

name: "Lizard"

on: pull_request

jobs:
  ccncheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - run: pip install lizard
      - run: lizard -l typescript -o report.txt -w ./src
      - run: gh pr review -F report.txt -r ${{ github.event.pull_request.html_url }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        if: ${{ failure() }}

これで、Pull Request に対して Review ができるようになりました。
以下は、実際の(手元になかなかいい例がなかったので閾値をめちゃくちゃ低く -T nloc=2 設定した)スクリーンショットです。

GitHub Actions の設定を加えるだけで運用できちゃいますので、コードが複雑になりがちでお困りの方はぜひお試しください💪


(追記)
この記事だと、コメントに全て書かれてしまいますが、Problem Matcher を使えば行ごとに Annotation をつけることが出来ました。この記事の方法だと run: gh... しているところも不要になって設定もスッキリするので、こっちの方がいいかもです!
https://zenn.dev/link/comments/de5bcf1460cb62

スタフェステックブログ

Discussion

ピン留めされたアイテム
Keisuke SatoKeisuke Sato

これも後から気づいたのですが、 .github/lizard.json とかを以下の様に作って、

{
  "problemMatcher": [
    {
      "owner": "lizard",
      "pattern": [
        {
          "regexp": "^(.+):(\\d+):\\s(.+):\\s(.+)$",
          "file": 1,
          "line": 2,
          "severity": 3,
          "message": 4
        }
      ]
    }
  ]
}

Workflow 内で - run: echo "::add-matcher::.github/lizard.json" してあげると、Annotation がついてよりリッチにできますね。

(記事に追記しろよという話)

Keisuke SatoKeisuke Sato

記事の上に 🐕 がいて思い出したけど、reviewdog 使ってあげればもっと高度なレビューが出来そう。

Keisuke SatoKeisuke Sato

だんだんコメントの方が充実してきている気がするけど、再び。

例えば雑コードだけどこうすると、GitHub Actions の Summary に出力できる。
(が、気にしなくなる未来しかなさそう、、、)

# ...
      - uses: actions/setup-node@v3
      - run: npm i csv-parser
      - run: lizard -l php --csv --output lizard.csv ./src
      - uses: actions/github-script@v6
        with:
          script: |
            const csv = require("csv-parser");
            const fs = require("fs");
            const results = [];

            fs.createReadStream("lizard.csv")
              .pipe(csv({
                headers: [
                  "nloc",
                  "ccn",
                  "token_count",
                  "parameter_count",
                  "length",
                  "location",
                  "filepath",
                  "method_name",
                  "method_long_name",
                  "start_line",
                  "end_line"
                ]
              }))
              .on('data', (data) => results.push(data))
              .on('end', async () => {
                await core.summary
                  .addHeading("Lizard Summary")
                  .addTable([
                    [
                      { data: "NLOC", header: true },
                      { data: "CCN", header: true },
                      { data: "token", header: true },
                      { data: "PARAM", header: true },
                      { data: "length", header: true },
                      { data: "Location", header: true },
                    ],
                    ...results.map(data => ([
                      data.nloc,
                      data.ccn,
                      data.token_count,
                      data.parameter_count,
                      data.length,
                      data.location,
                    ]))
                  ])
                  .write();
              });