golangで静的解析ツールを作ってみた

golangで静的解析ツールを作ってみた

先週末にgolang.tokyoの静的解析ハンズオンに参加し、静的解析ツールを作ってみました。 コードはGitHubにあります。

つくったツール

golangでAPIサーバーを開発しているときに、リクエストボディのjsonを構造体にマッピングしたいときは結構あると思います。 そんなときは構造体にjsonタグを書きます。 そうすると, json.Unmarshalしたときにjsonが構造体にマッピングされます。 しかしながら、多くの場合、構造体のフィールドとjsonのキーが一致しないことが多いですよね。 jsonのキーは多くの場合はスネークケースで記述されているからです。

今回作った静的解析ツールは構造体のフィールド名のスネークケースでjson tagを記述しないと怒ってくれるツールです。

イメージがつきにくいので、サンプルの構造体を記述します。 ↓こんな感じ

type Info struct {
	UserId string `json:id` // Bad "user_id" is correct.
	UserName string `json: user_name` // OK
}

一行目のようにスネークケースで記述されていない場合は、ツールによって、エラーを出すようにします。

(ちなみに構造体のフィールド名のスネークケースでjonタグを書くべきだ、というのは個人的な意見です。)

使ったライブラリ

今回はこのツールをgolangのgolang.org/x/tools/go/analysisパッケージを使うことによって実装しました。 このパッケージは手軽に静的解析ツールを作れることを目的としたパッケージです。

実装自体はものすごく簡単で、analysis.Analyzer構造体を実装することで、静的解析ツールを実装することができます。

analysis.Analyzer構造体にはName,Doc,Run,Require等のフィールドがあります。
Name,Doc,Run,Requireはそれぞれ、 静的解析ツールの名前、ドキュメント、実行する関数、依存する静的解析ツールに対応します。 ですので、静的解析のロジック自体はRun関数に書きます。

実装

Run関数が実際のロジックであると先程述べました。 今回はRun関数を以下のように定義しました。

func run(pass *analysis.Pass) (interface{}, error) {
	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	nodeFilter := []ast.Node{
		(*ast.StructType)(nil),
	}

	inspect.Preorder(nodeFilter, func(node ast.Node) {
		if structType, ok := node.(*ast.StructType); ok {
			for _, field := range structType.Fields.List {
				fieldName := field.Names[0].Name
				tag := field.Tag.Value
				if !Checker(fieldName, tag) {
					pass.Reportf(field.Tag.Pos(), "invalid snake case json tag")
				}
			}
		}
	})
	return nil, nil
}

今回はAnlyzer構造体の依存関係にinspect.Analyzerを指定しています。こうすることで、自分の静的解析ツールのrunメソッドが実行される前に、依存関係の静的解析ツールが実行され、自分の静的解析ツールで依存関係の静的解析ツールの結果を用いることができる。 今回の場合はinspect.Inspector構造体が提供する便利な関数を使うことができます。 inspect.Inspector構造体のPreorder関数で、ASTのフィルタリングができます。 この関数を使う必要がない場合には別にRequiresにinspect.Analyzerを指定しなくても良いです。

依存関係の静的解析ツールの結果はpass.ResultOfから、取得することができます。 inspect.PreorderはASTのフィルタリングを行い、該当するAST Nodeに対してだけ、引数の関数を実行します。 ちなみに以下の部分ではast.Nodeのスライスの初期化を行なっています。 (*ast.StructType)(nil)でnilを*ast.StructType型にキャストしています。

    nodeFilter := []ast.Node{
		(*ast.StructType)(nil),
	}

Preorder関数で構造体を表す、ast.StructTypeを取得したので、あとはフィールド名とタグ名を取得して、スネークケースかをチェックしています。 ast.StructType構造体についてはgodocを参照してください。

pass.Reportf関数はエラーを表示する関数です。astの構造体はPos()というソースコードの位置情報を持っているので、 位置情報とエラーメッセージを表示しています。

main関数

実際に独自のAnlyzerを呼び出すmain関数は以下のようになっています。

package main

import (
	"github.com/hikaru7719/jsontagchecker"
	"golang.org/x/tools/go/analysis/singlechecker"
)

func main() {
	singlechecker.Main(jsontagchecker.Analyzer)
}
singlechcker.Mainは引数で与えられた、一つのAnalyzerを実行する関数です。

実行結果

実際にターミナルから、コマンドを実行してみると、jsonタグがスネークケースで書かれていないところで、エラーメッセージが表示されました。

$ jsontagchecker sample/my.go
jsontagchecker/sample/my.go:5:21: invalid snake case json tag