shomanのブログ

ただの備忘録

メルカリインターンでGoの関数の複雑度を測るツールを作った話

こんにちは.先週、メルカリのオンラインインターンに参加してきました!

インターンの概要はここに載っています. mercan.mercari.com

最近いつも研究がやばいと言いながら、参加したい気持ちを抑えられませんでした…

概要

5日間でGoの静的解析を学び、ツールを作るという内容のオンラインインターンでした.

内2日はGoの静的解析についての講義、残りの3日間は開発というスケジュールです.

講義資料はここで公開されており、講義の録画も後日公開される予定みたいです.

講義も分かりやすいく、そもそもGo自体に静的解析周りの環境が充実していたこともあり、実装に集中できたと思います.

skeletonという静的解析のコードの雛形を作ってくれるツールはとても便利です.

開発はたまたま最初のビデオチャットで同じ部屋になった大学の友達のsff1019とチーム開発することにしました.

複雑度計測ツール

例えば、プルリクエスト(PR)をレビューしてる際に、そのソースコードの難易度がどれくらいなのかの目安を知りたいと思ったことはありませんか?

ソースコードの難易度については様々なメトリクスがありますが、今回はその中でもCyclomatic Complexity (循環的複雑度)とHalstead Complexity (日本語訳分からん) を計測する静的解析ツールを作りました. (複雑度の詳細については後述)

また、実際にGithub Actionsを利用することでPRの際に静的解析を走らせ、reviewdogというツールを使うことでPRのdiffに対しアノテーションをつける機能も実装しました.

https://user-images.githubusercontent.com/32924835/92326807-d7d0db00-f08f-11ea-940e-bdb9d6e81546.png

リポジトリは以下です (ツール自体とGithub Actionsのaction).

GitHub - shoooooman/go-complexity-analysis: Calculate complexities of golang functions with static analysis

GitHub - shoooooman/go-complexity-analysis-action: Show complexities of golang functions on pull requests with reviewdog

使い方

インストール

$ go get github.com/shoooooman/go-complexity-analysis/cmd/complexity

実行

$ go vet -vettool=$(which complexity) .

Github Actionsとの連携

PRに対し実行したい場合は、Githubリポジトリ.github/workflows/ 以下に下のようなyamlファイルを作成してください.

on: pull_request

jobs:
  reviewdog:
    runs-on: ubuntu-latest
    name: complexity analysis
    steps:
    - uses: actions/checkout@v2
    - uses: shoooooman/go-complexity-analysis-action@v1
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}

これでPRの際にactionが実行されアノテーションが表示されるはずです.

デフォルトでは Cyclomatic Complexity > 10 の関数のみに出力される設定になっていますが、これは変更することができます.

詳細な説明はリポジトリのREADMEをお読みください.

複雑度とは

そもそもCyclomatic ComplexityとかHalstead Complexityとか何?という話です.

Cyclomatic Complexity (循環的複雑度)

Wikipediaによるとプログラムの可読性に関係する指標で、以下のように定義されます.

M = E − N + 2P
ただし、
 M = 循環的複雑度
 E = グラフのエッジ数
 N = グラフのノード数
 P = 連結されたコンポーネントの数

こちらの記事も分かりやすかったです.

gocycloを使ってgo言語のプロダクトをシンプルに維持する|moli9ma|note

しかし、実際にソースコードの循環的複雑度を計測する場合には if, if else, for, switch などの分岐の数を数え計算することが多いようです.

今回の実装も関数定義の初期値を1とし、ブロック中の if, else if, for, case, ||, && の数を足すといった方法をとりました.

例えば以下のコードの場合、Cyclomatic Complexityは3になります.

func f(v int) {
    if v == 0 {      // 1つ目
        ...
    } else if v == 1 // 2つ目
        ...
    } else {
        ...
    }
}

こちらを参考に、閾値を設定できます.

循環的複雑度 複雑さの状態 バグ混入確率
10以下 非常に良い構造 25%
30以上 構造的なリスクあり 40%
50以上 テスト不可能 70%
75以上 いかなる変更も誤修正を生む 98%

Halstead Complexity

こちらはソースコードの難易度を表す指標です.

オペランドとオペレーターの数や種類によって計算されます.こちらのページを参考にしました.

Halsteadの計測の実装はチームメンバーのsff1019がやってくれました.

Maintainability Index (おまけ)

複雑度とは少し違いますが、保守性を表すMaintainability Indexというものが、上のCyclomatic ComplexityとHalstead Complexityを用いて計算できます.

せっかくなので、ツールにはこちらの指標も出力するように実装しました.

オリジナルの計算式をMicrosoft正規化したものを使用しました.

Normalized Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171)

Lines of Code (コードの行数) もコードの静的解析を行うことで取得できます.

改善すべき点

現時点ではPRを出した際に、diffのあるファイルに含まれる全ての関数についてCyclomatic Complexityを表示します.

本来は、diffを含む関数のみに出力すべきですが、範囲が広くなってしまっています.

diffの位置を取得しそれを静的解析ツールに渡せれば、その中でdiffを含む関数を調べ、その定義部分に出力するといったことができるのですが、良い方法が分かっていません.

何か良い案があれば教えていただけると嬉しいです.

ちなみにアノテーションではなくコメントで複雑度を表示しようとすると、Githubの仕様上、diffの前後数行までしかコメントをつけることができないため、厳しいです.

感想

5日間という短い間でしたが、インターン前と比べ静的解析やGoの仕様への (加えてGithub Actionsも) 理解が深まったと思います.

そして何より静的解析が楽しかったです.インターン後も時間があるときに何かツールを開発してみようと考えています.

CI連携について詰まった部分について話すと長くなりそうなので、また別の記事にでも書ければと思います.

関係者のみなさん、ありがとうございました!