Многие инструменты автоматической генерации кода полагаются на анализ синтаксического дерева, напримерgoimport,gomock,wireЭксперты неотделимы от анализа грамматики. Многие забавные и практичные инструменты могут быть реализованы на основе анализа дерева грамматики. В этой статье будут объединены примеры, показывающие, какast
Стандартные пакеты работают с синтаксическими деревьями.
Полный пример кода в этом посте можно найти здесь:ast-example
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:
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
Файл был максимально упрощен, но вывод его синтаксического дерева по-прежнему довольно велик. Возьмем несколько кратких пояснений.
Первый — это имя пакета, которому принадлежит файл, и расположение его объявления в файле:
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 . . }
....
Вы можете видеть, что синтаксическое дерево содержит 4Decl
запись, возьмем для примера первую запись, запись*ast.GenDecl
тип. Нетрудно видеть, что эта запись соответствует нашейimport
фрагмент кода. Начальную позицию (TokPos), позицию левой и правой скобок (Lparen, Rparen) и импортированный пакет (Specs) можно получить из синтаксического дерева.
Печатная буква синтаксического дерева исходит от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
}
Объединив комментарии и имена полей, мы, наверное, знаем значение каждого поля.Далее давайте подробно разберем структуру синтаксического дерева.
Узел узел
Все синтаксическое дерево состоит из разных узлов.Из комментариев к исходному коду мы можем узнать, что существует три основных типа узлов:
There are 3 main classes of nodes: Expressions and type nodes, statement nodes, and declaration nodes.
в ГоLanguage SpecificationПодробные спецификации и описания этих типов узлов можно найти в , а заинтересованные партнеры могут их подробно изучить, и я не буду их здесь раскрывать.
Но на самом деле в коде есть четвертый вид узлов: Spec 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()
}
....
)
Вы можете видеть, что все узлы наследуютNode
Интерфейс, который записывает начальную и конечную позицию узла. Помните из примера «Быстрый старт»Decls
? это точноdeclaration nodes. В дополнение к вышеупомянутым четырем типам узлов, которые используют интерфейсы для классификации, некоторые узлы не определяют дополнительно категории подразделения интерфейса, а только реализуютNode
Интерфейс, для удобства описания, в этой статье я обозначаю эти узлы какcommon node.$GOROOT/src/go/ast/ast.go
Перечисляет все узлы реализации всех, мы выбираем и выбираем несколько в качестве примеров, почувствуйте разницу между ними.
Expression and Type
Давайте сначала посмотрим на узел выражения.
$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
}
...
Indent
(идентификатор) представляет собой идентификатор, такой как имя пакета в примере быстрого запуска.Name
Поле — это узел выражения:
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 . }
...
Далее идет узел типа.
$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
}
...
Узел типа хорошо понятен, он содержит несколько составных типов, таких как тот, что появляется в Quick Start.StructType
,FuncType
иInterfaceType
.
Statement
Операторы присваивания, управляющие операторы (if, else, for, select...) и т. д. относятся к узлу оператора.
$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
}
...
Например, в Quick Start мыmain
Фрагмент программы в функции, присваивающей значение переменной a, принадлежитAssignStmt
:
...
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
Есть только 3 типа узлов Spec, а именноImportSpec
,ValueSpec
иTypeSpec
:
$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
}
ImportSpec
Представляет собой отдельный импорт,ValueSpec
представляет постоянную или переменную декларацию,TypeSpec
Затем укажите оператор TYPE. Например
В примере Quick Start появилосьImportSpec
иTypeSpec
import (
"context" // <--- 这里是一个ImportSpec node
)
// Foo 结构体
type Foo struct { // <--- 这里是一个TypeSpec node
i int
}
Соответствующий вывод можно увидеть в результате печати синтаксического дерева, и друзья могут найти его сами.
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
}
)
...
BadDecl
Представляет узел с синтаксической ошибкой;GenDecl
Используется для указания импорта, константы, объявления типа или переменной;FunDecl
Используется для представления объявлений функций.GenDecl
иFunDecl
Он появляется в примере «Быстрый старт», и друзья могут найти его сами.
Common 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 содержит все узлы, перечисленных выше мелких партнеров можно найти самостоятельно. Более подробные комментарии и конкретные поля структуры см. в исходном коде.
Все перечисленные типы узлов примерно завершены, из которых существует множество конкретных типов узлов, не перечислить их все, но в основном похожие, комментарии к исходному коду также относительно ясны, поэтому при более внимательном рассмотрении слишком поздно. Теперь, когда у нас есть общее представление о структуре всего синтаксического дерева, продемонстрируем его использование на нескольких примерах.
Пример
Добавьте параметры контекста ко всем методам интерфейса в файле
Для достижения этой функции нам нужно четыре шага:
- Пройтись по всему синтаксическому дереву
- Определите, был ли он импортирован
context
пакет, импортировать, если нет - Пройдитесь по всем методам интерфейса, чтобы определить, есть ли они в списке методов.
context.Context
Тип входного параметра, если нет добавляем его в первый параметр метода - Преобразуйте модифицированное синтаксическое дерево в код Go и выведите
Пройтись по синтаксическому дереву
Синтаксическое дерево глубокое, а отношения вложенности сложны. Если мы не можем полностью понять отношения между узлами и правилами вложенности, нам сложно написать правильный метод обхода самостоятельно. Но к счастьюast
Пакет уже предоставляет нам методы обхода:
$GOROOT/src/go/ast/ast.go
func Walk(v Visitor, node Node)
type Visitor interface {
Visit(node Node) (w Visitor)
}
Walk
Метод будет проходить по всему синтаксическому дереву в соответствии с методом поиска в глубину (порядок поиска в глубину) Нам нужно только реализовать его в соответствии с потребностями нашего бизнеса.Visitor
интерфейс.Walk
Вызывается каждый раз при обходе узлаVisitor.Visit
метод, передавая текущий узел. еслиVisit
возвращениеnil
, затем прекратите обход дочерних узлов текущего узла. этого примераVisitor
Реализация выглядит следующим образом:
// 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
}
добавить импорт
// 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
format
Пакет предоставляет нам функции преобразования,format.Node
будет следовать синтаксическому деревуgofmt
Вывод формата:
...
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)
}
Причинами этого явления являются:ast
Комментарии в синтаксическом дереве, сгенерированном пакетом, являются «свободно плавающими». Помните, что каждый узел имеетPos()
иEnd()
Метод определить его местоположение? Для узлов без комментариев синтаксическое дерево может правильно настроить их положение, но не может автоматически настроить положение узлов комментариев. Если мы хотим, чтобы аннотация появилась в правильном положении, мы должны вручную установить узелPos
иEnd
. Эта проблема упоминается в комментариях к исходному коду:
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).
В вопросе есть конкретные обсуждения, и чиновник признает, что это конструктивный недостаток, но его давно не улучшали. Среди них младший брат, который не мог ждать, предложил свой план:
Если вы действительно хотите изменить аннотированное синтаксическое дерево, вы можете попробовать это сделать. Хотя синтаксическое дерево действительно трудно модифицировать, оно все же может удовлетворить большинство задач генерации кода на основе анализа синтаксического дерева (gomock, wire и т. д.).
Ссылаться на
syslog.ravelin.com/how-to-mark…
Medile.com/@astrid.Germany...
Стек overflow.com/questions/3...
GitHub.com/golang/go/i…
GitHub.com/golang/go/i…
gowave.org/double/go/AST/…