LoginSignup
7
0

More than 1 year has passed since last update.

[Go] enum のヘルパー生成や網羅性チェックをするツール enumizer を作った

Last updated at Posted at 2022-12-19

はじめに

Go言語には列挙型を定義する機能がないため、代替として iota を使用したり、 type Color string のように定義した型の値を const ブロックで列挙したりすることが多いと思います。

type Sushi int

const (
    Salmon Sushi = iota
    Tuna
    Squid
)

type Color string

const (
    Red   Color = "red"
    Green Color = "blue"
    Blue  Color = "green"
)

これらの型定義と const での値の列挙を行っただけでは扱いにくく、通常 string 型への変換を行うメソッドや、初期化時/初期化後に値が定義域内に収まっているかのバリデーションを行う関数/メソッドを定義することになります。また、場合によってはその enum 相当の型が取り得る値のリストを取得したくなることもあります。これらのような関数/メソッドを生成するコードジェネレータはいくつかありますが、いずれも細かい部分で自分の要求を完全に満たすものではなかったので自作してみることにしました(検討したツールの例を以下に示します)。

これらのツールを検討した上で、自分のツールのコンセプトとしては以下のものが挙がりました。

  • 定義域をチェックするメソッドの名前は func (s Sushi) IsSushi() bool のような型名を含むものではなく、 func (s Sushi) IsValid() bool func (s Sushi) Validate() error のような型に関わらず共通のものとする
    • これらのメソッドを要求する interface を用意することで、 interface を満たす型であれば DB への書き込み前にバリデーションメソッドを呼び出し、不正な値が書き込まれるのを防ぐなどの実装が可能となる
  • enum 相当の型が取り得る値のリストを返す関数を生成する
  • デフォルト値を定義するなど、取り得る値を列挙するのとは別の目的で const ブロックを使用していた場合に区別できるようにするため、const ブロックにマーカーとなるコメントを手動でつけるようにする
  • コメントで細かい文法のある記法は使わない

また、コード生成とは異なりますが、同じマーカーコメントを利用して「switch 文に渡された enum の取り得る値すべてを case 群が網羅しているか」をチェックする静的解析ツールを実装できそうに思ったので合わせて作成することとしました。これは、例えば以下のようなコードがあったときに Squid の case がないのを警告するということです。

func Foo(sushi Sushi) {
    switch sushi {
    case Salmon:
        // do something
    case Tuna:
        // do something
    }
}

このようなチェック機構は Rust などの言語では標準的に利用でき、enum の取り得る値が増えた場合にすべての利用箇所を目でチェックして対応漏れがないかチェックする必要がなくなるので便利です。

以上を踏まえてGo言語における enum のヘルパー生成や網羅性チェックをするツール enumizer を作成したので、その使い方を紹介をします。

enumizer の紹介

リポジトリ内にある examples ディレクトリを用いて enumizer の使い方を説明します。

以下のように、 enum が取り得る値を列挙した const ブロックに enumizer:target のようなマーカーコメントを書きます。

examples/myenum/myenum.go
package myenum

type MyEnum int

// enumizer:target
const (
	A MyEnum = iota
	B
	C
)

$ enumizer generate ./examples/... を実行することで、以下のファイルが生成されます。

examples/myenum/enumizer.gen.go
// Code generated by enumizer; DO NOT EDIT.
package myenum

import "fmt"

var myEnumSet = map[MyEnum]struct{}{
	A: {},
	B: {},
	C: {},
}

func MyEnumList() []MyEnum {
	ret := make([]MyEnum, 0, len(myEnumSet))
	for v := range myEnumSet {
		ret = append(ret, v)
	}
	return ret
}

func (m MyEnum) String() string {
	switch m {
	case A:
		return "A"
	case B:
		return "B"
	case C:
		return "C"
	default:
		return "<unknown MyEnum>"
	}
}

func (m MyEnum) IsValid() bool {
	_, ok := myEnumSet[m]
	return ok
}

func (m MyEnum) Validate() error {
	if !m.IsValid() {
		return fmt.Errorf("MyEnum(%v) is invalid", m)
	}
	return nil
}

$ enumizer cover ./examples/... を実行することで、enum を受け取っている switch 文の case が enum の定義域をカバーできているかどうかチェックできます。
不足した case がある場合は、どの case が不足しているかが警告文からわかるようになっています。現状、default や余分な case が存在しているかどうかは気にせず無視します。  

examples/myenumuser/myenumuser.go
package myenumuser

import "github.com/neglect-yp/enumizer/examples/myenum"

func Foo(a myenum.MyEnum) {
	switch a {
	case myenum.B:
	}
}
$ enumizer cover ./examples/...
/<path to homedir>/enumizer/example/myenumuser/myenumuser.go:6:2: this switch statement doesn't cover enum variants. missing cases: A, C

Analyzer

enumizer cover の内部実装は golang.org/x/tools/go/analysis を用いているので、unitchecker や multichecker を用いて他の analyzer と組み合わせることが可能です。

おわりに

この記事では、Go言語における enum のヘルパー生成や網羅性チェックを行う自作ツールである enumizer を作成した経緯とその利用方法の紹介を行いました。少しでも皆様の開発の助けになればうれしいです。
まだ十分に運用できているわけではないので不完全な部分もあるかとは思いますが、興味を持っていただけたらフィードバックしてくださると励みになります。issue や PR も歓迎しています。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0