受Fatih Arslan关于为Go编写工具的精彩[演讲]以及Gary Bernhardt关于计算的[截屏]的启发,我决定自己用Go尝试一下AST遍历,准备更深入地研究这个问题。
这篇短文将只介绍如何进行绝对的基础知识。
- 将Go程序解析为抽象语法树
- 只用标准库来分析AST
在未来,我计划在这一领域涵盖更高级的主题。
让我们开始吧。
代码实例
首先,让我们看一下我们要解析的文件。
package main
import "fmt"
import "strings"
func main() {
hello := "Hello"
world := "World"
words := []string{hello, world}
SayHello(words)
}
// SayHello says Hello
func SayHello(words []string) {
fmt.Println(joinStrings(words))
}
// joinStrings joins strings
func joinStrings(words []string) string {
return strings.Join(words, ", ")
}
没有什么花哨的东西--简直就是一个过于复杂的Hello, World 程序。然而,即使是这一小段代码,也包含了许多有趣的元素,如函数、变量、注释、导入、导出和函数调用,这足以让我们的脚步变得轻松。
下一步是将该文件解析为AST。为此,我们像这样使用go/parser 包。
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, "test.go", nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
第一行使用go/token 包来创建一个新的FileSet,它代表一组源文件,我们需要用它来进行解析。
然后我们简单地调用parser.ParseFile ,模式是parser.ParseComments ,它解析所有的东西,包括注释,我们得到一个新的ast.File 作为返回值。这个ast.File 是一个Go源文件的表示,看起来像这样。
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
我们已经可以使用这个数据结构来开始分析我们的程序了。例如,我们可以使用node.Imports 字段列出我们所有的进口。
fmt.Println("Imports:")
for _, i := range node.Imports {
fmt.Println(i.Path.Value)
}
现在,这本身并不是非常有趣,但我认为即使是这个简单的例子,也不难想象它的作用。例如,对于整个代码库,我们可以分析所有的外部依赖关系,只用这几行代码就可以收集。
我们可以对ast.File 数据结构的其他部分做同样的工作,比如收集注释,或者函数。
fmt.Println("Comments:")
for _, c := range node.Comments {
fmt.Print(c.Text())
}
fmt.Println("Functions:")
for _, f := range node.Decls {
fn, ok := f.(*ast.FuncDecl)
if !ok {
continue
}
fmt.Println(fn.Name.Name)
}
注释看起来与导入相同,但对于函数,我们实际上是看文件中的所有声明(node.Decls),并检查它们是否是函数(*ast.FuncDecl)。我们可以进一步检查函数的主体或检查其他类型的声明。
这已经很不错了,但是你可以想象,像这样遍历AST可能会很麻烦,所以go/ast 包提供了ast.Inspect 函数,这让我们的工作变得更加漂亮和轻松。
func Inspect(node Node, f func(Node) bool)
Inspect 函数接收一个节点(比如我们的ast.File )和一个访问者函数,该函数对整个AST中遇到的每一个节点都会被调用,而AST是以深度为先进行的。
使用这个强大的工具,我们可以,例如,寻找我们代码中的返回语句。
ast.Inspect(node, func(n ast.Node) bool {
// Find Return Statements
ret, ok := n.(*ast.ReturnStmt)
if ok {
fmt.Printf("return statement found on line %d:\n\t", fset.Position(ret.Pos()).Line)
printer.Fprint(os.Stdout, fset, ret)
return true
}
return true
})
我们只需对我们感兴趣的节点类型做一个类型断言,然后对它做一些处理。在这种情况下,我们只需使用fset.Position() 来获取该语句所在的行,并使用go/printer 包的printer.Fprint() 函数来打印实际的代码行。
另一个用例可能是再次找到所有的函数,并找出它们是否被导出。
// Find Functions
fn, ok := n.(*ast.FuncDecl)
if ok {
var exported string
if fn.Name.IsExported() {
exported = "exported "
}
fmt.Printf("%sfunction declaration found on line %d: \n\t%s\n", exported, fset.Position(fn.Pos()).Line, fn.Name.Name)
return true
}
其结构与上述相同--我们断言该节点是一个函数声明,然后使用类型断言的节点来查找更多的信息。相当不错。
结语
我们对Go中基本AST遍历的短暂探索到此结束了。上面这个简短的例子只触及了与AST交互的表面,但展示了Go标准库中处理这类问题的良好API。
有了这样的API,再加上语言的简单性,难怪Go会有如此丰富的工具生态系统了。
另外,在我看来,这些技术不仅对建立复杂的通用工具有用,而且对以某种方式分析你自己的代码库或自动处理你的项目中非常具体的东西也有帮助。
这种可能性似乎是无穷无尽的,而且在你写的代码中走来走去,以一种自动化的方式看看什么是什么,这也是相当有趣的:)