🐕

[lizard]循環的複雑度(CCN)が高すぎるコードをブロックするGitHub Actionを作ってみた

2022/05/25に公開約6,500字

エレベータ―ピッチ

  • コードの複雑度(CCN、行数など)をCIに組み込むことで、複雑すぎるコードのマージを阻止
  • GitHub Actionとして、GitHub Marketplaceに公開し、容易に導入可能。
  • 引数としてしきい値などを容易にカスタマイズ可能。
  • 軽量なコンテナを使い1分以内に実行が可能
  • OSS+GitHub Actionsなのでプライベートリポジトリなどでも無課金で使用可能

概要

一般的に、アジャイル開発の現場においては、CIとしてテスト自動化を組み込むことが行われています。テスト自動化を行う事で、マージしたコードが、思いもよらない機能に対して、致命的な副作用を発生させていないことが保証され続けます。これによりエンジニアはより心理的安全性の高い環境で開発できます。

しかし、プロダクトが一定以上の規模になってくると、テストケースが十分に存在しており、カバレッジが高く保たれていたとしても、一部の複雑なコードの混入し、それが多くのコンポーネントと結合することで、保守性が下がってしまうことがあります。

特に、優先度を決める場面では、リファクタリングなどは軽視されがちです。客観的な指標を導入しておくことで、『今コードがどれだけ複雑なのか?』というのを可視化し、『気づいたときには手遅れだった』という状況を回避します。

こうした、複雑すぎるコードがいつの間にか混入することを防ぐために、CIの一環としてコードの複雑度を監視し続ける方法を求めていました。

そこで、lizardというOSSを使って、CIでCCNなどのコードメトリクスを測定し続けることによって、複雑すぎるコードがマージされることを防ぐための簡単なアクションを作成しました。

lizardとは

lizard循環的複雑度(CCN)をはじめとしたコードメトリクスを測定するためのOSSです。

CCNについては以下の記事がわかりやすいため、必要であれば参照してください。

ものすごくざっくりと解説すると、if分岐などを数えていて、多ければ多いほど複雑と判断されます。

先ほどの記事では以下のように目安を示しており概ね同意できます。

循環的複雑度 状態
1-10 安全なコードでテストしやすい
11-20 少し複雑なコード
21-40 複雑なコードでテストが難しくなる
41以上 やばいやつ。テスト不可能

lizardはこのほかにもコードの長さや引数の数なども測定できます。

lizardでは多くの言語でこれらのメトリックスを測定できます。

以下はlizardが対応している言語一覧です。

  • 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

lizardのインストール方法

lizardはPythonで開発しており、pipでインストールできます。

sudo pip install lizard

デフォルト設定で良ければ、コードメトリックスを測定したいフォルダにいき、以下のコマンドで測定可能です。

lizard PATH
  ================================================
    NLOC    CCN   token  PARAM  length  location  
  ------------------------------------------------
         2      1     16      1       2 surround_double_quotes@36-37@./entrypoint.py
  1 file analyzed.
  ==============================================================
  NLOC    Avg.NLOC  AvgCCN  Avg.token  function_cnt    file
  --------------------------------------------------------------
      126       2.0     1.0       16.0         1     ./entrypoint.py
  
  ===============================================================================================================
  No thresholds exceeded (nloc > 1000000 or cyclomatic_complexity > 15 or length > 1000 or parameter_count > 100)
  ==========================================================================================
  Total nloc   Avg.NLOC  AvgCCN  Avg.token   Fun Cnt  Warning cnt   Fun Rt   nloc Rt
  ------------------------------------------------------------------------------------------
         126       2.0     1.0       16.0        1            0      0.00    0.00

これと同じことを、GitHub Actionsで実行し、もしも設定値よりもCCNや行数が多かった場合、ジョブが失敗しマージできなくなるような方法を以下に示します。

GitHub Actionsでの導入方法

lizardを導入するためのGitHub Action『Lizard-Runner』を開発し公開しました。

従って、actions/checkout@v3actions/setup-python@v3と同様に、手軽に実行することが出来ます。

デフォルト設定でリポジトリに対してlizardを実行するだけの最小ケースであれば以下のようになります。

name: Lizard Runner
on:
  push:

jobs:
  lizard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Lizard Runner
        uses: Uno-Takashi/Lizard-Runner@v3

これだけでも以下の設定値で、lizardを実行してくれます。

!!!! Warnings (cyclomatic_complexity > 15 or length > 1000 or nloc > 1000000 or parameter_count > 100) !!!!
メトリックス 概要 デフォルト値
CCN 循環的複雑度 15
length コードの行数 1000
nloc 空白やコメントを引いたコード行数 1000000
parameter_count 引数の数 100

コードメトリックスのしきい値を超え、警告が出れば、アクションは失敗として終了します。問題がなければアクションは成功で終了します。

Lzard Runnerの失敗

ただ、デフォルト設定では緩すぎる値と厳しすぎる値が設定されており、プロダクトに合わせ適切に設定することが必要です。

また、既存のコードですでに警告が発生している場合、そのコードを修正するもしくは無視する設定も可能です。

以降Lizard-Runnerの設定例を示します。

Lizard Runnerの設定

Lizard RunnerはGitHub Actionsの引数として様々なパラメータをlizardに設定できます。

設定可能な引数の一覧はマーケットプレイスのREADMEに記載しています。
今回は主要な設定について説明します。

入力のほとんどは、lizardのオプション引数と同じ形式です。

しかしながら、一部は利便性のためにラップしています。

また、全てダブルクオーテーションで囲った形式にて指定します。

実行パスの指定

実行時、解析するパスを指定できます。
スペースで区切ることで複数のディレクトリが指定できますが、仕様上スペースの含んだディレクトリは指定できません。

      - name: Lizard Runner
        uses: Uno-Takashi/Lizard-Runner@v3
        with:
          path: "./src ./libs"

CCNのしきい値変更

CCNのしきい値はlizardと全く同じです。
15では少々厳しすぎるため、20程度を設定することをお勧めします。

      - name: Lizard Runner
        uses: Uno-Takashi/Lizard-Runner@v3
        with:
          CCN: "20"

解析言語の変更

解析する言語を指定できます。

lizardでは複数の言語を指定する場合、それぞれオプション引数として渡さなくてはならないです(lizard --language python --language javascript)。Lizard-Runnerはラップしておりスペースで区切ることでまとめて指定できます。

ただし、解析言語は指定しない場合全ての言語を解析してくれるため、ほとんどの場合は指定する必要がありません。複数の言語が混じったディレクトリに対して、個別に実行したい場合のみ、必要になるオプションです。

      - name: Lizard Runner
        uses: Uno-Takashi/Lizard-Runner@v3
        with:
          language: "python javascript cpp"

コード行数のしきい値変更

CCNと同じで、数値を指定します。

      - name: Lizard Runner
        uses: Uno-Takashi/Lizard-Runner@v3
        with:
          length: "300"

無視出来るエラー数を変更

既存コードの場合、複雑度の高いコードが含まれており、エラーになってしまう場合があります。
それに対する回避策は大きく分けて2つあると考えています。

  • 一時的に許容してそのうち直す
  • コードの複雑度を落とす

前者を選択した場合、Lizard-Runnerが毎回こけてしまうのは邪魔になってしまう恐れがあります。

そのため、許容する警告の数を指定します。

      - name: Lizard Runner
        uses: Uno-Takashi/Lizard-Runner@v3
        with:
          ignore_warnings: "2"

上記の例であれば2件まではメトリックスのいずれかで引っかかったとしてもエラー終了(ジョブが失敗)にはなりません。

無視するファイルを設定

テストコードなど無視したいコードがある場合があると思います。

その場合は、正規表現で無視するファイルを指定できます。
これもスペースで区切ると、複数与えられます。

      - name: Lizard Runner
        uses: Uno-Takashi/Lizard-Runner@v3
        with:
          exclude: "*/tests.py"

その他

その他の設定値はLizard Runner · Actions · GitHub Marketplaceを参照してください。

もちろん複数の設定値を同時に指定できます。

まとめ

以上、CCNをCIに組み込む方法でした。

自我自尊ではありますが、設定も簡単だし、副作用も少ないし、実行は早いし、結構いいアクションだと思っています。

複雑度をCIに組み込んで、皆さんも安全な開発をしましょう!!!

GitHubで編集を提案

Discussion

ログインするとコメントできます