在上一篇文章中,我展示了一个如何用Go遍历AST的基本例子。遍历和分析程序的AST的能力对于建立代码分析工具等是很有用的,但真正的乐趣是从我们操作程序的抽象语法树开始的,这使我们能够建立强大的开发工具。
在这篇文章中,我们将创建一个简单的工具,在文档方面做一些有用的事情。该工具将解析一个给定的Go源文件,对于每一个没有文档字符串的导出函数,它将吐出一个警告,并在文档字符串应该出现的地方创建一个// TODO: document exported function-placeholder注释。
虽然这个非常简单的工具可能不会改变世界,但它足够小,可以展示如何解析AST,操作它,然后把改变的代码写回去的一般概念。
让我们开始吧。
代码实例
首先,让我们看一下我们的源文件,名为test.go 。
func main() {
fmt.Println("testprogram")
DoStuff()
}
func unexportedFunction() {}
// Whatever does other stuff
func Whatever() {}
func AnExportedFunction() {}
func DoStuff() {}
// DoOtherStuff does other stuff
func DoOtherStuff() {}
我们的第一步是将这个test.go 文件解析成一个AST。
// parse file
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, "test.go", nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
我们的策略是识别所有没有Doc-string的导出函数,并在它们上面添加TODO 注释。为此,我们还需要识别并收集AST中的所有注释,以便能够在文件的Comments 列表中正确定位新的注释。
为了遍历AST,我们使用ast.Inspect 函数。
comments := []*ast.CommentGroup{}
ast.Inspect(node, func(n ast.Node) bool {
// collect comments
c, ok := n.(*ast.CommentGroup)
if ok {
comments = append(comments, c)
}
// handle function declarations without documentation
fn, ok := n.(*ast.FuncDecl)
if ok {
if fn.Name.IsExported() && fn.Doc.Text() == "" {
// print warning
fmt.Printf("exported function declaration without documentation
found on line %d: \n\t%s\n", fset.Position(fn.Pos()).Line, fn.Name.Name)
}
}
})
首先,我们识别并收集所有的ast.CommentGroup 节点,也就是代码中现有的注释。我们还识别ast.FuncDecl 节点,也就是函数声明,如果它们被导出并且它们的fn.Doc.Text() --Doc字符串--是空的,我们就打印一个警告,说明未记录的函数的位置和名称。
在确定了我们的目标后,我们需要通过将我们的TODO-comment纳入它们的Doc-string来操作它们。我们通过创建一个新的ast.Comment 和ast.CommentGroup 并将fn.Doc 设置为该注释组来实现这一目的。
// create todo-comment
comment := &ast.Comment{
Text: "// TODO: document exported function",
Slash: fn.Pos() - 1,
}
// create CommentGroup and set it to the function's documentation comment
cg := &ast.CommentGroup{
List: []*ast.Comment{comment},
}
fn.Doc = cg
这里重要的是,我们需要将新创建的ast.Comment 节点的Slash 属性设置为fn.Pos() - 1 ,以正确定位注释。这是确定的函数在文件中的位置,表示为它的偏移量,我们把它减去1,以达到函数的正上方的行。
在这之后,我们将node'sComments 设置为我们收集的CommentGroup 列表,这样它们就被渲染了,并将AST,用go/printer 包漂亮地打印出来,写入一个叫做new.go 的文件。
// set ast's comments to the collected comments
node.Comments = comments
// write changed AST to file
f, err := os.Create("new.go")
defer f.Close()
if err := printer.Fprint(f, fset, node); err != nil {
log.Fatal(err)
}
new.go 中的结果看起来像这样,就像我们计划的那样。
func main() {
fmt.Println("testprogram")
DoStuff()
}
func unexportedFunction() {}
// Whatever does other stuff
func Whatever() {}
// TODO: document exported function
func AnExportedFunction() {}
// TODO: document exported function
func DoStuff() {}
// DoOtherStuff does other stuff
func DoOtherStuff() {}
就是这样。 :)
完整的例子代码可以在这里找到
结论
由于软件工程的复杂性,好的开发者工具对于提高开发者的效率至关重要。尽管Go已经有了一个强大的工具生态系统,但建立自己的工具的能力也带来了一些有趣的可能性,特别是当涉及到为自己的独特需求明确定制的工具时。
Go在标准库中提供了良好的、有据可查的库来编写这种定制的工具。此外,在我看来,构建此类工具所需的知识和技能也能提高人们对语言本身的理解。
编写自己的工具的另一个好处是,使用自己亲手制作的工具来促进自己的开发流程,这种感觉实在是太好了。)