只需一种选择,将以下内容改述为中文:仅通过Go的静态分析工具对实现代码进行遍历
首先
在开发过程中,可以使用Go的静态分析工具golang.org/x/tools/go/analysis,并使用golang.org/x/tools/go/analysis/passes/inspect来遍历语法树。然而,由于该工具会遍历除了实现代码以外的内容,因此在创建专注于实现代码的静态分析工具时可能会妨碍。
再接下来,我们将介绍如何使用 Nodes 进行遍历,并解释如何用它来排除测试文件和生成文件。
使用预购进行扫描
考虑一种静态分析工具,用于列出已定义的函数名称。通过使用 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进行遍历
节点比先序遍历稍微复杂一些,但可以更细致地控制,并且可以排除特定的子树。
与先前预订(Preorder)的区别在于回调函数被调用两次,针对一个节点。第一次调用发生在处理子节点之前,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
}
这将产生与之前使用前序遍历相同的结果。
$ 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