Руководство по использованию синтаксического дерева Golang AST и примеры

Go

Многие инструменты автоматической генерации кода полагаются на анализ синтаксического дерева, например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 содержит все узлы, перечисленных выше мелких партнеров можно найти самостоятельно. Более подробные комментарии и конкретные поля структуры см. в исходном коде.

Все перечисленные типы узлов примерно завершены, из которых существует множество конкретных типов узлов, не перечислить их все, но в основном похожие, комментарии к исходному коду также относительно ясны, поэтому при более внимательном рассмотрении слишком поздно. Теперь, когда у нас есть общее представление о структуре всего синтаксического дерева, продемонстрируем его использование на нескольких примерах.

Пример

Добавьте параметры контекста ко всем методам интерфейса в файле

Для достижения этой функции нам нужно четыре шага:

  1. Пройтись по всему синтаксическому дереву
  2. Определите, был ли он импортированcontextпакет, импортировать, если нет
  3. Пройдитесь по всем методам интерфейса, чтобы определить, есть ли они в списке методов.context.ContextТип входного параметра, если нет добавляем его в первый параметр метода
  4. Преобразуйте модифицированное синтаксическое дерево в код 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).

В вопросе есть конкретные обсуждения, и чиновник признает, что это конструктивный недостаток, но его давно не улучшали. Среди них младший брат, который не мог ждать, предложил свой план:

github.com/dave/dst

Если вы действительно хотите изменить аннотированное синтаксическое дерево, вы можете попробовать это сделать. Хотя синтаксическое дерево действительно трудно модифицировать, оно все же может удовлетворить большинство задач генерации кода на основе анализа синтаксического дерева (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/…