只需一种选择,将以下内容改述为中文:仅通过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
广告
将在 10 秒后关闭
bannerAds