Manipulation中的基本AST操作的方法

98 阅读4分钟

上一篇文章中,我展示了一个如何用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.Commentast.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在标准库中提供了良好的、有据可查的库来编写这种定制的工具。此外,在我看来,构建此类工具所需的知识和技能也能提高人们对语言本身的理解。

编写自己的工具的另一个好处是,使用自己亲手制作的工具来促进自己的开发流程,这种感觉实在是太好了。)

资源