はじめに
Goの静的解析ツールは golang.org/x/tools/go/analysis
を使うことで開発でき、構文木を走査するのに golang.org/x/tools/go/analysis/passes/inspect
が使える。しかし実装コード以外も走査されるため、実装コードに焦点を当てた静的解析ツールを作る際に邪魔になる。
ここでは Nodes
を使った走査を紹介し、それを用いてテストファイルとジェネレータで生成されたファイルを除外する方法を説明する。
Preorderを使った走査
定義した関数名を一覧する静的解析ツールについて考える。 Preorder
を使うとかなりそれらしいものがシンプルに書ける。
package funcdecl
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: "funcdecl",
Doc: `find function declarations`,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
inspect.Preorder([]ast.Node{
(*ast.FuncDecl)(nil), // 関心のあるノードの種類の値を列挙する(この例では関数定義のみ)
}, func(node ast.Node) {
f := node.(*ast.FuncDecl)
pass.Reportf(f.Pos(), `found %s`, f.Name)
})
return nil, nil
}
しかしこれには問題があり、テストファイルとジェネレータで生成されたファイルも対象となってしまう。以下の例で a_test.go
はテストファイル、最後のキャッシュはビルド由来の生成されたファイルである。これらを除外したい。
$ funcdecl ./...
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:3:1: found main
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:7:1: found Foo
/Users/ichiban/src/funcdecl/testdata/src/a/a_test.go:7:1: found TestFoo
/Users/ichiban/Library/Caches/go-build/be/bef6acc9728a13887659154cd4c2d9b8fb8e3d8d505ef63aeba17380c0b75212-d:34:1: found init
/Users/ichiban/Library/Caches/go-build/be/bef6acc9728a13887659154cd4c2d9b8fb8e3d8d505ef63aeba17380c0b75212-d:40:1: found main
Nodesを使った走査
Nodes
は Preorder
よりもやや複雑だがより細かい制御ができ、特定のサブツリーを除外することができる。
Preorder
との違いはコールバック関数にあり、 ひとつのノードに対して2回呼ばれる。一度目は子ノードを処理する前で、引数の push
には true
が渡される。また、戻り値として false
を返すと子ノードをルートとするサブツリーを処理しなくなる。二度目は子ノード(とそれをルートとするサブツリー)を処理した後で、 push
として false
が渡され、戻り値は無視される。
package funcdecl
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: "funcdecl",
Doc: `find function declarations`,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
inspect.Nodes([]ast.Node{
(*ast.FuncDecl)(nil), // 関心のあるノードの種類の値を列挙する(この例では関数定義のみ)
}, func(n ast.Node, push bool) bool {
if !push { // 子ノードを処理する前にだけ関心がある
return false
}
f := n.(*ast.FuncDecl)
pass.Reportf(f.Pos(), `found %s`, f.Name)
return false // 関数定義はネストしないのでサブツリーには関心がない
})
return nil, nil
}
これは前述の Preorder
を用いたものと同じ結果になる。
$ funcdecl ./...
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:3:1: found main
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:7:1: found Foo
/Users/ichiban/src/funcdecl/testdata/src/a/a_test.go:7:1: found TestFoo
/Users/ichiban/Library/Caches/go-build/be/bef6acc9728a13887659154cd4c2d9b8fb8e3d8d505ef63aeba17380c0b75212-d:34:1: found init
/Users/ichiban/Library/Caches/go-build/be/bef6acc9728a13887659154cd4c2d9b8fb8e3d8d505ef63aeba17380c0b75212-d:40:1: found main
これを用いて問題のファイルを除外していく。
テストファイルの除外
テストファイルはファイル名の末尾が _test.go
なのでそれを条件に除外できる。
package funcpattern
import (
"go/ast"
"regexp"
"strings"
"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: "funcdecl",
Doc: `find function declarations`,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
inspect.Nodes([]ast.Node{
(*ast.File)(nil), // 関数定義だけでなくファイルにも関心が出た
(*ast.FuncDecl)(nil),
}, func(n ast.Node, push bool) bool {
if !push {
return false
}
switch n := n.(type) {
case *ast.File:
f := pass.Fset.File(n.Pos())
return !strings.HasSuffix(f.Name(), "_test.go") // 末尾が `_test.go` であるサブツリーには関心がない
case *ast.FuncDecl:
pass.Reportf(f.Pos(), `found %s`, n.Name)
return false
default:
panic(n)
}
})
return nil, nil
}
テストファイルが除外された。
$ funcdecl ./...
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:3:1: found main
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:7:1: found Foo
/Users/ichiban/Library/Caches/go-build/be/bef6acc9728a13887659154cd4c2d9b8fb8e3d8d505ef63aeba17380c0b75212-d:34:1: found init
/Users/ichiban/Library/Caches/go-build/be/bef6acc9728a13887659154cd4c2d9b8fb8e3d8d505ef63aeba17380c0b75212-d:40:1: found main
ジェネレータで生成されたファイルの除外
ジェネレータで生成されたファイルは、コメントに DO NOT EDIT
と含まれるように、利用者としてはそのコードを変更すべきでない。そのため静的解析ツールのレポートは邪魔になる。
実はこのコメント // Code generated * DO NOT EDIT.
はファイルが生成されたものかどうかを判別するのに使ってよいことになっている。
package funcdecl
import (
"go/ast"
"regexp"
"strings"
"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: "funcdecl",
Doc: `find function declarations`,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
inspect.Nodes([]ast.Node{
(*ast.File)(nil),
(*ast.FuncDecl)(nil),
}, func(n ast.Node, push bool) bool {
if !push {
return false
}
switch n := n.(type) {
case *ast.File:
f := pass.Fset.File(n.Pos())
if strings.HasSuffix(f.Name(), "_test.go") {
return false
}
return !generated(n) // ジェネレータで生成されたファイルのサブツリーには関心がない
case *ast.FuncDecl:
pass.Reportf(n.Pos(), `found %s`, n.Name)
return false
default:
panic(n)
}
})
return nil, nil
}
// https://github.com/golang/go/issues/13560#issuecomment-288457920
var pattern = regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`)
// ファイルのどこかに生成されたことを表すコメントがある
func generated(f *ast.File) bool {
for _, c := range f.Comments {
for _, l := range c.List {
if pattern.MatchString(l.Text) {
return true
}
}
}
return false
}
ジェネレータで生成されたファイルも除外された。
$ funcdecl ./...
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:3:1: found main
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:7:1: found Foo
おわりに
Nodes
を使った走査を紹介し、それを用いてテストファイルとジェネレータで生成されたファイルを除外する方法を説明した。
とはいえ、毎回明示的にこれらのファイルを除外するのは面倒だ。あらかじめ除外していてくれるラッパー ichiban/prodinspect
を作ったので活用してほしい。これを使うと最初の Preorder
の例とほぼ同じコードで意図した結果が得られる。
package funcdecl
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"github.com/ichiban/prodinspect"
)
var Analyzer = &analysis.Analyzer{
Name: "funcdecl",
Doc: `find function declarations`,
Requires: []*analysis.Analyzer{prodinspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[prodinspect.Analyzer].(*prodinspect.Inspector)
inspect.Preorder([]ast.Node{
(*ast.FuncDecl)(nil),
}, func(n ast.Node) {
f := n.(*ast.FuncDecl)
pass.Reportf(f.Pos(), `found %s`, f.Name)
})
return nil, nil
}
$ funcdecl ./...
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:3:1: found main
/Users/ichiban/src/funcdecl/testdata/src/a/a.go:7:1: found Foo