Goでenumのswitchをcheckできるツールを作りました

Goでenumのswitchをcheckできるツールを作りました

Goでenumswitchcheckできるツール switchecker を作りました。

switchecker

switchecker は Goのソースコード内でswitch文で enum を用いた値のパターンマッチングにおいてenumで宣言されている case をすべて網羅できているかをチェックしてくれるツールです。と、ここまで言いましたが、Goenum ?と思いますよね。厳密にはGoenumは存在しないです。ここで enum と表現してしまっているものは下記のようなコードになります。連続した定数定義と iota を使用したりするアレです。 おそらくみなさん一度は Go enum でググって出てきたコードではないでしょうか。

type language int

const (
    golang language = iota
    swift
    objectivec
    ruby
    typescript
)

func function(x language) {
    switch x {
    case swift:
        println("swift")
    case ruby:
        println("ruby")
    case golang:
        println("golang")
    case typescript:
        println("typescript")
    case objectivec:
        println("objectivec")
    default:
        panic("unexpected default")
    }
}

これをこの記事では形式的に enum と表現しちゃいます。やりたいことに対してわかりやすいのと「連続した定数定義と iota を使用したりするアレ」って書くのは長いし(このテクニックに名前がついていたら教えて下さい)

用法

例えば先程の例に出したコードを2分割にして enum.gouse.go と2つに分かれているとします。

enum.go

package main

type language int

const (
    golang language = iota
    swift
    objectivec
    ruby
    typescript
)

use.go

package main

func function(x language) {
    switch x {
    case swift:
        println("swift")
    case ruby:
        println("ruby")
    case golang:
        println("golang")
    case typescript:
        println("typescript")
    case objectivec:
        println("objectivec")
    default:
        panic("unexpected default")
    }
}

この場合は -sourceenum.go を渡して、 チェックしたいファイルである use.go-target に渡して使用をします。

$ switchecker -source=enum.go -target=use.go

globも使えます

$ switchecker -source=*.go -target=*.go

デフォルトでは *.go 渡すようになっているのでこの場合は省略も可能です。

$ switchecker 

実行して成功するとこんな感じです。

$ ls
enum.go main.go use.go

$ switchecker
Succesfull!! $ switchecker -source=*.go -target=*.go

ファイルが増えてきたら , 区切りで対象を増加できます。

$ switchecker -source=enum.go -target=use.go,use2.go

そしてhelpです。

$ switchecker --help                                                                              
Usage of switchecker:
  -source string
        Source go files are containing enum definition. Multiple specifications can be specified separated by ,. e.g) *.go,pkg/**/*.go  (default "*.go")
  -target string
        Target go files are containing to use enum. Multiple specifications can be specified separated by ,. e.g) *.go,pkg/**/*.go  (default "*.go")
  -verbose
        Enabled verbose log

用量

プロダクトで使っているCIに導入してこれにチェックさせるのが一つ。もう一つは単純に手元で走らせるのも一つの使いみちかな。と思います。ただエディタで編集したときにチェック。とかは考えてないです。

モチベーションとか仕様とか

使いたいシチュエーションとしてはツールを少し雑に作っていたときに enum を用いて関数の引数に渡して switch で分岐して。みたいな書き方をしていたら、あとから enum の要素を増やす必要が出てきたので増やした。そしたら実行時にちょっとバグった。コンパイルでは検知できないけど、ここも検知してほしいな。作ってみるか。と思って作ってみました。

まあ実のところ languageintに名前がついただけなやつなので case 1000 とか書けてしまいこれは switchecker には引っかかりません。。現状はこういった case は無視して、golang,swift...など、連続で定数定義されたものすべてが一つのswitchの中に入っているか check してくれる機能に絞っています。 名前のついていない case 等は実装社の責任としています。

あとはトップレベルで宣言されたものだけを現在対応しています。func の中で同じ構文で書いていても対応していないです。 func の中で宣言するケースはきっと多くないだろう。というのと ast 等で底を解析して他の型と区別させるのが大変。という理由です。

どこまでサポートしているか、想定しているか。を厳密に知りたい方はコードを読むのが一番正しいとは思うのでぜひ見てみてください。
ちなみにテストを見るとサポート範囲のイメージがつかめると思います。

技術

使ったパッケージや構成をさらっと。

go/ast-source の引数の解析部分で golang.org/x/tools/go/packages-target で渡したファイルの型情報と -source から作った構造体の情報とマッピングしてチェックする。そういった流れになっています。

-target の型情報取得はgo/typesでも行けるのかなあ。と思ったのですがConfig.Checkを使ってみた所、渡した引数のGoファイルの型情報は取得できているが、標準のパッケージではない自作のパッケージ等がimportされている場合はそっちの型情報が取得できずに Config.Error が返ってきてしまいました。少し試行錯誤しましたがイマイチimport先の型情報の取得を go/types を使って取得するやりかたがわからなくて、golang.org/x/tools/go/packages の方ではimport先の型情報まで取得できたのでこちらを使用しています。

まとめ

switchecker のリポジトリはこちらです。

github.com

個人的に ast や 型情報みたいなメタな部分を使って効率化を測るツールを作るのが好きで、 その中でもコード量が少なめかつ割と愛用していきそうななツールができたと思っています。 まだできたばかりなので CI にまで組み込めてませんが手元で動かす感じでは期待した通りの動きで満足です。近いうちにCIにも組み込んでみてもっと改善してい後と思います。

enum のチェッカー欲しかったんだよなあ。これいいかも。switchecker って名前がいいね。と思ったそこのあなた

スターください 🌟

おしまい \(^o^)/