on
golangで静的解析ツールを作ってみた
Tweetgolangで静的解析ツールを作ってみた
先週末に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)
}
実行結果
実際にターミナルから、コマンドを実行してみると、jsonタグがスネークケースで書かれていないところで、エラーメッセージが表示されました。
$ jsontagchecker sample/my.go
jsontagchecker/sample/my.go:5:21: invalid snake case json tag