ast
本篇中的代码的完整示例能够在这里找到:ast-examplegit
Quick Start./demo.go
package main
import (
"go/ast"
"go/parser"
"go/token"
"log"
"path/filepath"
)
func main() {
fset := token.NewFileSet()
// 这里取绝对路径,方便打印出来的语法树能够转跳到编辑器
path, _ := filepath.Abs("./demo.go")
f, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
log.Println(err)
return
}
// 打印语法树
ast.Print(fset, f)
}
复制代码
demo.go:golang
package main
import (
"context"
)
// Foo 结构体
type Foo struct {
i int
}
// Bar 接口
type Bar interface {
Do(ctx context.Context) error
}
// main方法
func main() {
a := 1
}
复制代码
demo.go
首先是文件所属的包名,和其声明在文件中的位置:bash
0 *ast.File {
1 . Package: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:1:1
2 . Name: *ast.Ident {
3 . . NamePos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:1:9
4 . . Name: "main"
5 . }
...
复制代码
Decls
...
6 . Decls: []ast.Decl (len = 4) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:3:1
9 . . . Tok: import
10 . . . Lparen: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:3:8
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:4:2
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"context\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:5:1
22 . . }
....
复制代码
Decl*ast.GenDeclimport
ast.File
$GOROOT/src/go/ast/ast.go函数
// 该结构体位于标准包 go/ast/ast.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节点整个语法树由不一样的node组成,从源码注释中能够得知主要有以下三种node:
There are 3 main classes of nodes: Expressions and type nodes, statement nodes, and declaration nodes.
在Go的Language Specification中能够找到这些节点类型详细规范和说明,有兴趣的小伙伴能够深刻研究一下,在此不作展开。
但实际在代码,出现了第四种node:Spec Node,每种node都有专门的接口定义:
$GOROOT/src/go/ast/ast.go
...
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
...
// A Spec node represents a single (non-parenthesized) import,
// constant, type, or variable declaration.
//
type (
// The Spec type stands for any of *ImportSpec, *ValueSpec, and *TypeSpec.
Spec interface {
Node
specNode()
}
....
)
复制代码
NodeDeclsNode$GOROOT/src/go/ast/ast.go
Expression and Type
先来看expression node。
$GOROOT/src/go/ast/ast.go
...
// An Ident node represents an identifier.
Ident struct {
NamePos token.Pos // identifier position
Name string // identifier name
Obj *Object // denoted object; or nil
}
...
复制代码
IndentName
0 *ast.File {
1 . Package: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:1:1
2 . Name: *ast.Ident { <----
3 . . NamePos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:1:9
4 . . Name: "main"
5 . }
...
复制代码
接下来是type node。
$GOROOT/src/go/ast/ast.go
...
// A StructType node represents a struct type.
StructType struct {
Struct token.Pos // position of "struct" keyword
Fields *FieldList // list of field declarations
Incomplete bool // true if (source) fields are missing in the Fields list
}
// Pointer types are represented via StarExpr nodes.
// A FuncType node represents a function type.
FuncType struct {
Func token.Pos // position of "func" keyword (token.NoPos if there is no "func")
Params *FieldList // (incoming) parameters; non-nil
Results *FieldList // (outgoing) results; or nil
}
// An InterfaceType node represents an interface type.
InterfaceType struct {
Interface token.Pos // position of "interface" keyword
Methods *FieldList // list of methods
Incomplete bool // true if (source) methods are missing in the Methods list
}
...
复制代码
StructTypeFuncTypeInterfaceType
Statement
赋值语句,控制语句(if,else,for,select...)等均属于statement node。
$GOROOT/src/go/ast/ast.go
...
// An AssignStmt node represents an assignment or
// a short variable declaration.
//
AssignStmt struct {
Lhs []Expr
TokPos token.Pos // position of Tok
Tok token.Token // assignment token, DEFINE
Rhs []Expr
}
...
// An IfStmt node represents an if statement.
IfStmt struct {
If token.Pos // position of "if" keyword
Init Stmt // initialization statement; or nil
Cond Expr // condition
Body *BlockStmt
Else Stmt // else branch; or nil
}
...
复制代码
mainAssignStmt
...
174 . . . Body: *ast.BlockStmt {
175 . . . . Lbrace: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:18:13
176 . . . . List: []ast.Stmt (len = 1) {
177 . . . . . 0: *ast.AssignStmt { <--- 这里
178 . . . . . . Lhs: []ast.Expr (len = 1) {
179 . . . . . . . 0: *ast.Ident {
180 . . . . . . . . NamePos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:19:2
181 . . . . . . . . Name: "a"
...
复制代码
Spec Node
ImportSpecValueSpecTypeSpec
$GOROOT/src/go/ast/ast.go
// An ImportSpec node represents a single package import.
ImportSpec struct {
Doc *CommentGroup // associated documentation; or nil
Name *Ident // local package name (including "."); or nil
Path *BasicLit // import path
Comment *CommentGroup // line comments; or nil
EndPos token.Pos // end of spec (overrides Path.Pos if nonzero)
}
// A ValueSpec node represents a constant or variable declaration
// (ConstSpec or VarSpec production).
//
ValueSpec struct {
Doc *CommentGroup // associated documentation; or nil
Names []*Ident // value names (len(Names) > 0)
Type Expr // value type; or nil
Values []Expr // initial values; or nil
Comment *CommentGroup // line comments; or nil
}
// A TypeSpec node represents a type declaration (TypeSpec production).
TypeSpec struct {
Doc *CommentGroup // associated documentation; or nil
Name *Ident // type name
Assign token.Pos // position of '=', if any
Type Expr // *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes
Comment *CommentGroup // line comments; or nil
}
复制代码
ImportSpecValueSpecTypeSpecImportSpecTypeSpec
import (
"context" // <--- 这里是一个ImportSpec node
)
// Foo 结构体
type Foo struct { // <--- 这里是一个TypeSpec node
i int
}
复制代码
在语法树的打印结果中能够看到对应的输出,小伙伴们可自行查找。
Declaration Node
Declaration node也只有三种:
$GOROOT/src/go/ast/ast.go
...
type (
// A BadDecl node is a placeholder for declarations containing
// syntax errors for which no correct declaration nodes can be
// created.
//
BadDecl struct {
From, To token.Pos // position range of bad declaration
}
// A GenDecl node (generic declaration node) represents an import,
// constant, type or variable declaration. A valid Lparen position
// (Lparen.IsValid()) indicates a parenthesized declaration.
//
// Relationship between Tok value and Specs element type:
//
// token.IMPORT *ImportSpec
// token.CONST *ValueSpec
// token.TYPE *TypeSpec
// token.VAR *ValueSpec
//
GenDecl struct {
Doc *CommentGroup // associated documentation; or nil
TokPos token.Pos // position of Tok
Tok token.Token // IMPORT, CONST, TYPE, VAR
Lparen token.Pos // position of '(', if any
Specs []Spec
Rparen token.Pos // position of ')', if any
}
// A FuncDecl node represents a function declaration.
FuncDecl struct {
Doc *CommentGroup // associated documentation; or nil
Recv *FieldList // receiver (methods); or nil (functions)
Name *Ident // function/method name
Type *FuncType // function signature: parameters, results, and position of "func" keyword
Body *BlockStmt // function body; or nil for external (non-Go) function
}
)
...
复制代码
BadDeclGenDeclFunDeclGenDeclFunDecl
Common Node
除去上述四种类别划分的node,还有一些node不属于上面四种类别:
$GOROOT/src/go/ast/ast.go
// Comment 注释节点,表明单行的 //-格式 或 /*-格式的注释.
type Comment struct {
...
}
...
// CommentGroup 注释块节点,包含多个连续的Comment
type CommentGroup struct {
...
}
// Field 字段节点, 能够表明结构体定义中的字段,接口定义中的方法列表,函数前面中的入参和返回值字段
type Field struct {
...
}
...
// FieldList 包含多个Field
type FieldList struct {
...
}
// File 表示一个文件节点
type File struct {
...
}
// Package 表示一个包节点
type Package struct {
...
}
复制代码
Quick Start示例包含了上面列举的全部node,小伙伴们能够自行查找。更为详细的注释和具体的结构体字段请查阅源码。
全部的节点类型大体列举完毕,其中还有许多具体的节点类型未能一一列举,但基本上都是大同小异,源码注释也比较清晰,等用到的时候再细看也不迟。如今咱们对整个语法树的构造有了基本的了解,接下来经过几个示例来演示具体用法。
示例为文件中全部接口方法添加context参数
实现这个功能咱们须要四步:
contextcontext.Context
遍历语法树
ast
$GOROOT/src/go/ast/ast.go
func Walk(v Visitor, node Node) 复制代码
type Visitor interface {
Visit(node Node) (w Visitor)
}
复制代码
WalkVisitorWalkVisitor.VisitVisitnilVisitor
// Visitor
type Visitor struct {
}
func (v *Visitor) Visit(node ast.Node) ast.Visitor {
switch node.(type) {
case *ast.GenDecl:
genDecl := node.(*ast.GenDecl)
// 查找有没有import context包
// Notice:没有考虑没有import任何包的状况
if genDecl.Tok == token.IMPORT {
v.addImport(genDecl)
// 不须要再遍历子树
return nil
}
case *ast.InterfaceType:
// 遍历全部的接口类型
iface := node.(*ast.InterfaceType)
addContext(iface)
// 不须要再遍历子树
return nil
}
return v
}
复制代码
添加import
// addImport 引入context包
func (v *Visitor) addImport(genDecl *ast.GenDecl) {
// 是否已经import
hasImported := false
for _, v := range genDecl.Specs {
imptSpec := v.(*ast.ImportSpec)
// 若是已经包含"context"
if imptSpec.Path.Value == strconv.Quote("context") {
hasImported = true
}
}
// 若是没有import context,则import
if !hasImported {
genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote("context"),
},
})
}
}
复制代码
为接口方法添加参数
// addContext 添加context参数
func addContext(iface *ast.InterfaceType) {
// 接口方法不为空时,遍历接口方法
if iface.Methods != nil || iface.Methods.List != nil {
for _, v := range iface.Methods.List {
ft := v.Type.(*ast.FuncType)
hasContext := false
// 判断参数中是否包含context.Context类型
for _, v := range ft.Params.List {
if expr, ok := v.Type.(*ast.SelectorExpr); ok {
if ident, ok := expr.X.(*ast.Ident); ok {
if ident.Name == "context" {
hasContext = true
}
}
}
}
// 为没有context参数的方法添加context参数
if !hasContext {
ctxField := &ast.Field{
Names: []*ast.Ident{
ast.NewIdent("ctx"),
},
// Notice: 没有考虑import别名的状况
Type: &ast.SelectorExpr{
X: ast.NewIdent("context"),
Sel: ast.NewIdent("Context"),
},
}
list := []*ast.Field{
ctxField,
}
ft.Params.List = append(list, ft.Params.List...)
}
}
}
}
复制代码
将语法树转换成Go代码
formatformat.Nodegofmt
...
var output []byte
buffer := bytes.NewBuffer(output)
err = format.Node(buffer, fset, f)
if err != nil {
log.Fatal(err)
}
// 输出Go代码
fmt.Println(buffer.String())
...
复制代码
输出结果以下:
package main
import (
"context"
)
type Foo interface {
FooA(ctx context.Context, i int)
FooB(ctx context.Context, j int)
FooC(ctx context.Context)
}
type Bar interface {
BarA(ctx context.Context, i int)
BarB(ctx context.Context)
BarC(ctx context.Context)
}
复制代码
context.Context
一些坑与不足
至此咱们已经完成了语法树的解析,遍历,修改以及输出。但细心的小伙伴可能已经发现:示例中的文件并无出现一行注释。这的确是有意为之,若是咱们加上注释,会发现最终生成文件的注释就像迷途的羔羊,彻底找不到本身的位置。好比这样:
//修改前
type Foo interface {
FooA(i int)
// FooB
FooB(j int)
FooC(ctx context.Context)
}
// 修改后
type Foo interface {
FooA(ctx context.
// FooB
Context, i int)
FooB(ctx context.Context, j int)
FooC(ctx context.Context)
}
复制代码
astPos()End()PosEnd
Whether and how a comment is associated with a node depends on the interpretation of the syntax tree by the manipulating program: Except for Doc and Comment comments directly associated with nodes, the remaining comments are "free-floating" (see also issues #18593, #20744).
issue中有具体的讨论,官方认可这是一个设计缺陷,但仍是迟迟未能改进。其中有位火烧眉毛的小哥提供了本身的方案:
若是实在是要对有注释的语法树进行修改,能够尝试一下。 虽然语法树的确存在修改困难问题,但其仍是能知足大部分基于语法树分析的代码生成工做了(gomock,wire等等)。
参考