この記事ではGo(Un)Conferenceで発表したGoにおけるLinterツールの作成方法をまとめる。
- Go(Un)Conference(Goあんこ)LT大会 5kg
- golang.org/x/tools/go/analysisで静的解析ツールを自作する #gounco
TL;DR
- 2018年末にGoのLinterツール用ライブラリが刷新された
- https://godoc.org/golang.org/x/tools/go/analysis
- モジュールとして静的解析ルールを作成・自由に組み合わせることができるようになった
golang.org/x/tools/go/analysis
パッケージを使うと自作Linterツールが簡単に作れる- https://godoc.org/golang.org/x/tools/go/analysis
- ざっくり言うと静的解析ロジックを実装し、
Reportf
メソッドで結果を出力するだけ - 他の
Analyzer
の結果を利用したりもできる
- テストも直感的に書くことができる
- https://godoc.org/golang.org/x/tools/go/analysis/analysistest
- テスト用のGoファイルを作ってそれを読み込む仕組みが提供されている
- Linterを自作するときはtenntennさんのツールを使うととても簡単
実際に静的解析コマンドを実装したサンプルリポジトリは以下になる。
golang.org/x/tools/go/analysisとは
golang.org/x/tools/go/analysis
パッケージはGoで静的解析ツールを作成するための仕組みを提供するパッケージだ。詳細については@tenntennさんがmercariのブログにまとめてくれている。
このパッケージを使うと静的解析ロジックをモジュール化することができ、自由に組み合わせたりすることができる。
利用例でみると、すでにgo vet
コマンドの静的解析ロジックは分割・モジュール化され、analysis/passes
以下に各モジュールとして配置されている。
モジュールはanalysis.Analyzer構造体を実装することで構成される。
go vet
コマンドのmain
関数は以下のようになっており、各モジュールのAnalyzer
オブジェクトを読み込んでいるだけなのがわかるはずだ。
// https://github.com/golang/tools/blob/master/go/analysis/cmd/vet/vet.go
func main() {
multichecker.Main(
// the traditional vet suite:
asmdecl.Analyzer,
assign.Analyzer,
atomic.Analyzer,
atomicalign.Analyzer,
bools.Analyzer,
buildtag.Analyzer,
// ...
nilness.Analyzer,
)
}
静的解析モジュールを作成するための基本的な知識
MercariのブログとGoDocを見てもらえばいいのだが、golang.org/x/tools/go/analysis
パッケージで静的解析モジュールを作る際には以下の要点を押さえておけばよい。
Analyzer
各静的解析のモジュールの実態がAnalyzer
構造体となる。Analyzer
構造体のRun
メソッドに静的解析ロジックを実装することで静的解析が実現する。Analyzer
では他のAnalyzer
に解析結果を提供する際のResultType
、依存する他のAnalyzer
を指定するRequires
などのフィールドが存在する。
type Analyzer struct {
// ...
// ロジック本体。戻り値はなくても良いが、Resultとして他のAnalyzerに結果を渡す場合は返す。
Run func(*Pass) (interface{}, error)
// ...
// このAnalyzerが実行時に利用したい他のAnalyzer
Requires []*Analyzer
// このAnalyzerが他のAnalyzerに提供する情報。Runメソッドの戻り値
ResultType reflect.Type
// 同じAnalyzer内で解析対象のパッケージをまたいで解析情報を利用したい場合はFactという概念を利用してやり取りする
FactTypes []Fact
}
Pass
静的解析は実行時に受け取った解析対象のパッケージに対してAnalyzer
のRun
メソッドを実行していく。
Run
メソッドの引数として解析対象パッケージの情報を持つのがPass
構造体だ。
type Pass struct {
Analyzer *Analyzer // the identity of the current analyzer
// 解析対象パッケージの静的解析用の情報
Fset *token.FileSet // file position information
Files []*ast.File // the abstract syntax tree of each file
OtherFiles []string // names of non-Go files of this package
Pkg *types.Package // type information about the package
TypesInfo *types.Info // type information about the syntax trees
TypesSizes types.Sizes // function for computing sizes of types
// ...
// 他のAnalyzerのReslutを利用したい場合はこの中から受け取る
ResultOf map[*Analyzer]interface{}
// Factをやりとりするときの関数
ImportObjectFact func(obj types.Object, fact Fact) bool
// ...
}
静的解析中に利用するデータ概念
Analyzer
やPass
の説明中に出てきた3種類のデータの概念が以下だ。
- Result
- https://godoc.org/golang.org/x/tools/go/analysis#Analyzer
- その
Analyzer
に解析結果を提供するときに定義する
- Diagnostic
- https://godoc.org/golang.org/x/tools/go/analysis#Diagnostic
- CLIにしたときに出力されるエラー情報(
github.com/johndue/pkg/bar.go: 5:2: some analysis error
のような) Printf
関数の用に使えるPass.Reportfメソッドがあるので、実際この構造体をそのまま扱うことはない(と思う)
- Facts
- https://godoc.org/golang.org/x/tools/go/analysis#Fact
- 同じAnalyzer内で解析対象のパッケージをまたいで解析情報を利用できる。
自作のLinterを作ってみる
ここからは実際に自作のLinterを作ってみる。まず最初は@tenntennさんが公開しているskeleten
コマンドを使うと簡単にテンプレートが生成される。
mylinter
という名前で静的解析モジュールのテンプレートを作成するときは以下のように使う。
$ go get github.com/tenntenn/gosa/skeleton
$ skeleton mylinter
mylinter
├── mylinter.go # 静的解析ロジックを書く本体
├── mylinter_test.go
└── testdata
└── src
└── a # テストを書くときのテストケースパッケージ
└── a.go
これで静的解析ロジックをmylinter.go
の中のAnalyzer.Run
に自作の静的解析ロジックを実装していくだけでよい。
package mylinter
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var Analyzer = &analysis.Analyzer{
Name: "mylinter",
Doc: Doc,
Run: run,
Requires: []*analysis.Analyzer{
inspect.Analyzer,
},
}
const Doc = "mylinter is ..."
func run(pass *analysis.Pass) (interface{}, error) {
// 省略
}
このままAnalyzer
をコマンドとして実行したい場合は以下のようなcmd/main.go
ファイルを用意すればよいだろう。
// cmd/main.go
package main
import (
"github.com/budougumi0617/mylinter"
"golang.org/x/tools/go/analysis/multichecker"
"golang.org/x/tools/go/analysis/passes/inspect"
)
func main() {
multichecker.Main(
// skeletonで生成したAnalyzerの初期コードの場合はinspect.Analyzerの結果に依存している
inspect.Analyzer,
mylinter.Analyzer,
)
}
ここではmultichecker.Main
関数で定義しているが、Main
関数の実装はいくつか提供されている。
一つのAnalyzer
だけを使ったLinterなのか?複数のAnalyzer
を使ったLinterなのか?などによって使い分ける。
- https://godoc.org/golang.org/x/tools/go/analysis/singlechecker
- https://godoc.org/golang.org/x/tools/go/analysis/unitchecker
- https://godoc.org/golang.org/x/tools/go/analysis/multichecker
あとは通常どおりgo buid
すればLinterコマンドがもう利用できる。(以下はサンプルリポジトリを実行した例)
$ go build -o icheck ./cmd/main.go
# testdataディレクトリ以下のサンプルコードのimport構成が特殊なので
# GOPATHをちょっと変えているが、本来はGOPATHの再定義は必要ない
$ GOPATH=`pwd`/testdata:$GOPATH ./icheck ./testdata/src/...
/Users/shimizubudougumi0617/importcheck/testdata/src/handler/h.go:7:2: \
handler must not include "repository"
ASTの探索について。
準備は簡単なのだが、実際に難しいところは目的の静的解析ロジックを組み立てるところだろう(逆にいうと本質的な部分にフォーカスできるようになっているとも言える)。 抽象構文木(AST)については@tenntennさんのQiitaの記事や@motemenさんのgit bookを見るとわかりやすい。
skeleton
で生成したコードにもふくまれているが、inspect
パッケージを使うと簡単にASTを走査できるのでswitch
文を組み立てるだけでパースを開始できる。
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// 調べたいNodeの型でフィルタリングできる
nodeFilter := []ast.Node{
(*ast.Ident)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
switch n := n.(type) {
case *ast.Ident:
_ = n
}
})
return nil, nil
}
あとはシンプルなAnalyzer
の実装を見ていくとなんとなくやり方が見えてくると思う(私はまだ見えていない)。
Analyzerのテストを用意する
analysistest
パッケージを使うと、簡単にAnalyzer
をテストすることができる。
下記のように静的解析でエラーを出したいテスト用のサンプルコードを準備するだけだ。
エラーを出したい行に対して、// want
と始めて期待出力文字列をコメントしておくだけでよい。
// testdata/src/handler/h.go
package handler
import (
"repository" // want "handler must not include \"repository\""
"service"
)
...
あとは用意したサンプルコードを読み込んで、Analyzer
を実行するテストケースを用意する。skeleteon
コマンドでテンプレートを生成した場合、xxx_test.go
にそのようなテストケースがすでに生成されている。
テストコードには「./testdata/src
ディレクトリにある"a"
パッケージを一緒に生成したAnalyzer
で静的解析する」テストケースがすでにあるので、testdata/src/a/a.go
ファイルに自分が実装した静的解析ロジックで見つけたいエラーを書いておくだけだ。
package mylinter_test
import (
"testing"
"github.com/budougumi0617/mylinter"
"golang.org/x/tools/go/analysis/analysistest"
)
func Test(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, mylinter.Analyzer, "a")
}
skeleton
で生成したコードでそのままgo test
すれば、サンプルコードに書かれた// want
が満たされずFAILするのがわかる。
$ go test
--- FAIL: Test (0.03s)
analysistest.go:311: a/a.go:4: no diagnostic was reported matching "pattern"
FAIL
exit status 1
FAIL github.com/budougumi0617/mylinter 0.042s
その他のTips
静的解析を実装するときに参考にしたいコード
そもそも静的解析でどんなことができるのか?あの静的解析はどのようにロジックを組めばできるのだろう?という疑問もあるはずだ。
そういう疑問を持ったときははまずgolang.org/x/tools/go/analysis/passes
以下のパッケージを読むといいだろう。go vet
の静的解析の実装が全てAnalyzer
ベースになっている、
ExportObjectFact
など、Facts
を使った実装はgolang.org/x/tools/go/analysis/passes/printf/printf
パッケージで行われている。
Result
についてはgolang.org/x/tools/go/analysis/passes/inspect/inspect
を参考にすれば良い(他のモジュールからの使い方についてはskeleton
コマンドで生成したコードを見ればわかるだろう)。
テストケースの書き方について
テスト用のサンプルコードはtestdata
ディレクトリ以下に作成することになるが、analysistest
パッケージでテストを実行するときはtestdata
ディレクトリがGOPATH
になる。なので、たとえばrepository
パッケージをimport
するhandler
パッケージというサンプルコードを作ったとすると、ディレクトリ構成は以下のようになる。
$ tree testdata
testdata
└── src
├── domain
│ └── d.go
├── handler
│ └── h.go
├── repository
│ └── r.go
└── service
└── s.go
// testdata/src/handler/h.go
package handler
import (
"encoding/json"
"net/http"
"repository" // want "handler must not include \"repository\""
"service"
)
// Do anything...
フラグ
CLIツールの場合フラグ処理を入れたくなるのが常だ。analysis
パッケージを使った実装した場合、実行時のフラグはAnalyzer.Flags
に入って渡ってくる。具体的な呼び出し方はMercariのブログを見るとよいだろう。
type Analyzer struct {
// ...
// Flags defines any flags accepted by the analyzer.
// The manner in which these flags are exposed to the user
// depends on the driver which runs the analyzer.
Flags flag.FlagSet
// ...
}
既存Linterツールの移行について
すでにgolang.org/x/tools/go/analysis
パッケージ以前の仕組みでLinterを作成していた方もいるだろう。
既存Linterツールの移行(マイグレーション)についてはizumin5210さんやdaisuzuさんの記事が参考になる。
- golang.org/x/tools/analysis を理解する #golang #golangtokyo
- golang.tokyo 静的解析Dayでgolang.org/x/tools/go/analysisを使ってみた
終わりに
私は今までLinterを作るのは難しそうだなと思っていたが、golang.org/x/tools/go/analysis
で提供されるようになった仕組みを使えば、静的解析ロジック以外の殆どを気にしなくてすむ。また静的解析ロジックそのものもヘルパーモジュールを使うことでだいぶラクに実装できる。
今回サンプルとして実装したLinterツールはハードコーディングだらけで使い物にならないので、設定ファイルから条件を読み込むようにしたりして実際の業務で使えるLinterを作ってみたいと思う。
また、私は英語がちゃんと読めないので、日本語でいろいろな情報を書いてくださっている先輩Gopherのみなさんに感謝しかない(ましてやジェネレータまで提供してくれている!)。
参考
- package analysis - GoDoc
- Goにおける静的解析のモジュール化について - Mercari Engineering Blog
- モジュール化された静的解析の実装を追ってみよう #golang - Qita
- github.com/tenntenn/gosa/skeleton
- Qiitaで「“AST user:tenntenn”」と検索
- GoのためのGo
- 静的解析で型を扱う #golang - Qiita
- Go言語の golang/go パッケージで初めての構文解析
- golang.org/x/tools/analysis を理解する #golang #golangtokyo
- golang.tokyo 静的解析Dayでgolang.org/x/tools/go/analysisを使ってみた