【go语言】AST抽象语法树详解&实践之扫描代码生成错误码文档

背景

为了能识别出代码中抛出错误码的地址和具体的错误码值,再根据错误码文件获取到错误码的具体值和注释,方便后续的排错,这里使用AST进行语法分析获取到代码中的目标对象。

一、编译过程

在开始解析代码之前先补充了解一下编译过程

编译过程是将高级语言源代码转换为目标机器代码或其他中间表示的过程。它通常包括以下几个主要阶段:

词法分析(Lexical Analysis):

  • 输入:源代码
  • 输出:词法单元(tokens)
  • 任务:将源代码分解为基本的词法单元,如关键字、标识符、运算符等

语法分析(Syntax Analysis):

  • 输入:词法单元序列
  • 输出:语法树(Abstract Syntax Tree,AST)或其他中间表示
  • 任务:根据语法规则检查词法单元序列的结构,构建语法树表示源代码的语法结构。

语义分析(Semantic Analysis):

  • 输入:语法树或中间表示
  • 输出:语义信息
  • 任务:检查源代码的语义正确性,捕捉并处理语法上合法但语义上错误的结构,生成符号表等。

中间代码生成(Intermediate Code Generation):

  • 输入:语法树或中间表示
  • 输出:中间代码
  • 任务:将语法树转换为一种中间表示,以便进行后续优化和目标代码生成。

优化(Optimization):

  • 输入:中间代码
  • 输出:优化后的中间代码
  • 任务:对中间代码进行各种优化,提高程序性能、减小代码体积等。

目标代码生成(Code Generation):

  • 输入:优化后的中间代码
  • 输出:目标机器代码或汇编代码
  • 任务:将中间代码转换为目标机器代码,根据目标平台生成可执行文件。

链接(Linking):

  • 输入:目标机器代码或汇编代码,可能包括库文件
  • 输出:可执行文件
  • 任务:将多个目标文件链接在一起,解决符号引用、地址重定位等问题,生成最终的可执行文件。

二、AST

AST 指的是 Abstract Syntax Tree(抽象语法树),它是编程语言中源代码语法结构的一种抽象表示形式。AST 通常是一个树形结构,用于表示程序的语法层次关系,从而方便进行语法分析和处理。

在编程语言中,源代码是由一系列的词法单元(token)组成的,而 AST 则将这些词法单元按照语法规则组织成一个树形结构。每个节点代表源代码中的一个语法结构,例如表达式、语句、函数声明等。

先看一下demo代码和对应的生成的部分AST树:

package main

import (
	"errors"
	"log"
)

var DivisionByZero = errors.New("除0错误")
var LessThanZero = errors.New("小于0错误")

func divide(a, b int) (int, error) {
	if b == 0 {
		log.Default().Panic(DivisionByZero)
		return 0, DivisionByZero
	}
	if a < 0 {
		log.Default().Panic(LessThanZero)
		return 0, LessThanZero
	}
	return a / b, nil
}
     0  *ast.File {
     1  .  Package: example.go:1:1
     2  .  Name: *ast.Ident {
     3  .  .  NamePos: example.go:1:9
     4  .  .  Name: "main"
     5  .  }
     6  .  Decls: []ast.Decl (len = 4) {
     7  .  .  0: *ast.GenDecl {
     8  .  .  .  TokPos: example.go:3:1
     9  .  .  .  Tok: import
    10  .  .  .  Lparen: example.go:3:8
    11  .  .  .  Specs: []ast.Spec (len = 2) {
    12  .  .  .  .  0: *ast.ImportSpec {
    13  .  .  .  .  .  Path: *ast.BasicLit {
    14  .  .  .  .  .  .  ValuePos: example.go:4:2
    15  .  .  .  .  .  .  Kind: STRING
    16  .  .  .  .  .  .  Value: "\"errors\""
    17  .  .  .  .  .  }
    18  .  .  .  .  .  EndPos: -
    19  .  .  .  .  }
    20  .  .  .  .  1: *ast.ImportSpec {
    21  .  .  .  .  .  Path: *ast.BasicLit {
    22  .  .  .  .  .  .  ValuePos: example.go:5:2
    23  .  .  .  .  .  .  Kind: STRING
    24  .  .  .  .  .  .  Value: "\"log\""
    25  .  .  .  .  .  }
    26  .  .  .  .  .  EndPos: -
    27  .  .  .  .  }
    28  .  .  .  }
    29  .  .  .  Rparen: example.go:6:1
    30  .  .  }
    31  .  .  1: *ast.GenDecl {
    32  .  .  .  TokPos: example.go:8:1
    33  .  .  .  Tok: var
    34  .  .  .  Lparen: -
    35  .  .  .  Specs: []ast.Spec (len = 1) {
    36  .  .  .  .  0: *ast.ValueSpec {
    37  .  .  .  .  .  Names: []*ast.Ident (len = 1) {
    38  .  .  .  .  .  .  0: *ast.Ident {
    39  .  .  .  .  .  .  .  NamePos: example.go:8:5
    40  .  .  .  .  .  .  .  Name: "DivisionByZero"
    41  .  .  .  .  .  .  .  Obj: *ast.Object {
    42  .  .  .  .  .  .  .  .  Kind: var
    43  .  .  .  .  .  .  .  .  Name: "DivisionByZero"
    44  .  .  .  .  .  .  .  .  Decl: *(obj @ 36)
    45  .  .  .  .  .  .  .  .  Data: 0
    46  .  .  .  .  .  .  .  }
    47  .  .  .  .  .  .  }
    48  .  .  .  .  .  }
    49  .  .  .  .  .  Values: []ast.Expr (len = 1) {
    50  .  .  .  .  .  .  0: *ast.CallExpr {
    51  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {
    52  .  .  .  .  .  .  .  .  X: *ast.Ident {
    53  .  .  .  .  .  .  .  .  .  NamePos: example.go:8:22
    54  .  .  .  .  .  .  .  .  .  Name: "errors"
    55  .  .  .  .  .  .  .  .  }
    56  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    57  .  .  .  .  .  .  .  .  .  NamePos: example.go:8:29
    58  .  .  .  .  .  .  .  .  .  Name: "New"
    59  .  .  .  .  .  .  .  .  }
    60  .  .  .  .  .  .  .  }
    61  .  .  .  .  .  .  .  Lparen: example.go:8:32
    62  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    63  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
    64  .  .  .  .  .  .  .  .  .  ValuePos: example.go:8:33
    65  .  .  .  .  .  .  .  .  .  Kind: STRING
    66  .  .  .  .  .  .  .  .  .  Value: "\"除0错误\""
    67  .  .  .  .  .  .  .  .  }
    68  .  .  .  .  .  .  .  }
    69  .  .  .  .  .  .  .  Ellipsis: -
    70  .  .  .  .  .  .  .  Rparen: example.go:8:45
    71  .  .  .  .  .  .  }
    72  .  .  .  .  .  }
    73  .  .  .  .  }
    74  .  .  .  }
    75  .  .  .  Rparen: -
    76  .  .  }

1. Token

在编程语言中,Token(词法单元)是源代码中的最小语法单元,是编译器或解释器在进行词法分析时识别和处理的基本单位。Token 是源代码经过词法分析器处理后得到的标识符,它代表了源代码中的不可分割的词法结构。

不同类型的编程语言有不同的 Token 类型,一般包括以下几类:

  1. 关键字(Keywords): 表示编程语言的保留字,具有特殊含义。例如,在 C 语言中,ifelsewhile 等就是关键字。

  2. 标识符(Identifiers): 表示变量、函数、类等的名称。标识符需要遵循一定的命名规则。例如,variableName 是一个标识符。

  3. 常量(Literals): 表示固定值的词法单元,包括整数、浮点数、字符串等。例如,423.14"Hello, World!" 都是常量。

  4. 运算符(Operators): 表示执行操作的符号,如加法、减法、乘法等。例如,+-* 是运算符。

  5. 分隔符(Delimiters): 表示源代码结构的分隔符,如括号、分号、逗号等。例如,(); 是分隔符。

  6. 注释(Comments): 表示注释内容,编译器或解释器通常会忽略它们。例如,// This is a comment

Token 的产生是由词法分析器(Lexer 或 Scanner)负责的,它扫描源代码,将字符序列组合成有意义的 Token。Token 提供给语法分析器(Parser)使用,用于构建 AST(抽象语法树)等进一步的语法分析和语义分析。

下面是一个简单的示例,展示了一个小型程序的源代码和对应的一些 Token:

# 源代码 x = 10 + 5

对应的 Token:

Token(IDENT, "x") 
Token(ASSIGN, "=") 
Token(INT, "10") 
Token(PLUS, "+") 
Token(INT, "5")

在这个例子中,Token(IDENT, "x") 表示一个标识符 Token,Token(ASSIGN, "=") 表示一个赋值操作符 Token,Token(INT, "10") 表示一个整数常量 Token,以此类推。

Go 语言中的 Token类型:

2. FileSet

先看代码:使用 token.NewFileSet() 创建一个新的 FileSet 对象,并使用 parser.ParseFile 函数解析 Go 代码文件。

// 解析 Go 代码文件
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)

token 包中的 FileSet 类型是用于跟踪源代码文件和位置信息的结构。FileSet 用于维护源代码文件的集合,每个文件都关联有唯一的标识符,并记录了源代码中各个位置的行号、列号等信息。

 让我们看一下parser.ParseFile ()函数源码:

func ParseFile(fset *token.FileSet, filename string, src any, mode Mode) (f *ast.File, err error) {
	if fset == nil {
		panic("parser.ParseFile: no token.FileSet provided (fset == nil)")
	}

	// get source
	text, err := readSource(filename, src)
	if err != nil {
		return nil, err
	}

	var p parser
	defer func() {
		if e := recover(); e != nil {
			// resume same panic if it's not a bailout
			bail, ok := e.(bailout)
			if !ok {
				panic(e)
			} else if bail.msg != "" {
				p.errors.Add(p.file.Position(bail.pos), bail.msg)
			}
		}

		// set result values
		if f == nil {
			// source is not a valid Go source file - satisfy
			// ParseFile API and return a valid (but) empty
			// *ast.File
			f = &ast.File{
				Name:  new(ast.Ident),
				Scope: ast.NewScope(nil),
			}
		}

		p.errors.Sort()
		err = p.errors.Err()
	}()

	// parse source
	p.init(fset, filename, text, mode)
	f = p.parseFile()

	return
}

参数:

fset *token.FileSet: 用于记录位置信息的文件集合。

filename string: 源文件的文件名。

src any: 源代码,可以是字符串、字节数组或 io.Reader

mode Mode: 控制解析器的模式,例如是否跳过对象解析阶段。

返回值:

f *ast.File: 解析后的 AST 树,表示整个源文件的语法结构。

err error: 解析过程中的错误,如果有的话。

内部实现:

parser 结构体是用于实际解析的解析器对象。

readSource 函数用于获取源代码文本,它可以从文件或其他来源获取。

p.parseFile() 调用实际执行解析过程,返回解析得到的 AST。

错误处理:

如果源代码无法读取,返回的 AST 是空的,但不为 nil。

如果源代码读取成功但存在语法错误,返回的 AST 包含 ast.Bad* 节点来表示错误的源代码片段。

多个错误通过 scanner.ErrorList 返回,按源代码位置排序。

panic 恢复:

解析器在遇到错误时可能会触发 panic,但会通过 recover 恢复,确保程序不会崩溃。

如果 panic 是 bailout 类型且有错误消息,将错误添加到解析器的错误列表中。

 让我们再看一下其中的初始化函数p.init():解析器对象的初始化过程,设置了解析器的一些基本属性,初始化了文件对象和词法分析器,并准备开始解析源代码。

func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
	p.file = fset.AddFile(filename, -1, len(src))
	eh := func(pos token.Position, msg string) { p.errors.Add(pos, msg) }
	p.scanner.Init(p.file, src, eh, scanner.ScanComments)

	p.top = true
	p.mode = mode
	p.trace = mode&Trace != 0 // for convenience (p.trace is used frequently)
	p.next()
}

继续看一下核心的解析函数:解释看注释

// parseFile 解析整个 Go 源文件,返回相应的 ast.File 节点。
func (p *parser) parseFile() *ast.File {
	if p.trace {
		defer un(trace(p, "File"))
	}

	// 如果在扫描第一个标记时存在错误,则不必继续解析。
	// 可能根本不是有效的 Go 源文件。
	if p.errors.Len() != 0 {
		return nil
	}

	// 包子句
	doc := p.leadComment
	pos := p.expect(token.PACKAGE)
	// Go 规范:包子句不是声明;
	// 包名不会出现在任何范围中。
	ident := p.parseIdent()
	if ident.Name == "_" && p.mode&DeclarationErrors != 0 {
		p.error(p.pos, "无效的包名 _")
	}
	p.expectSemi()

	// 如果在解析包子句时存在错误,则不必继续解析。
	// 可能根本不是有效的 Go 源文件。
	if p.errors.Len() != 0 {
		return nil
	}

	var decls []ast.Decl
	if p.mode&PackageClauseOnly == 0 {
		// 导入声明
		for p.tok == token.IMPORT {
			decls = append(decls, p.parseGenDecl(token.IMPORT, p.parseImportSpec))
		}

		if p.mode&ImportsOnly == 0 {
			// 包体的其余部分
			prev := token.IMPORT
			for p.tok != token.EOF {
				// 继续接受导入声明以容忍错误,但是报告警告。
				if p.tok == token.IMPORT && prev != token.IMPORT {
					p.error(p.pos, "导入必须出现在其他声明之前")
				}
				prev = p.tok

				decls = append(decls, p.parseDecl(declStart))
			}
		}
	}

	// 构建 ast.File 对象
	f := &ast.File{
		Doc:       doc,
		Package:   pos,
		Name:      ident,
		Decls:     decls,
		FileStart: token.Pos(p.file.Base()),
		FileEnd:   token.Pos(p.file.Base() + p.file.Size()),
		Imports:   p.imports,
		Comments:  p.comments,
		GoVersion: p.goVersion,
	}
	var declErr func(token.Pos, string)
	if p.mode&DeclarationErrors != 0 {
		declErr = p.error
	}
	if p.mode&SkipObjectResolution == 0 {
		resolveFile(f, p.file, declErr)
	}

	return f
}

 核心的解析函数resolveFile:调用ast.Walk函数获得代码的AST树

func resolveFile(file *ast.File, handle *token.File, declErr func(token.Pos, string)) {
	pkgScope := ast.NewScope(nil)
	r := &resolver{
		handle:   handle,
		declErr:  declErr,
		topScope: pkgScope,
		pkgScope: pkgScope,
		depth:    1,
	}

	for _, decl := range file.Decls {
		ast.Walk(r, decl)
	}

	r.closeScope()
	assert(r.topScope == nil, "unbalanced scopes")
	assert(r.labelScope == nil, "unbalanced label scopes")

	// resolve global identifiers within the same file
	i := 0
	for _, ident := range r.unresolved {
		// i <= index for current ident
		assert(ident.Obj == unresolved, "object already resolved")
		ident.Obj = r.pkgScope.Lookup(ident.Name) // also removes unresolved sentinel
		if ident.Obj == nil {
			r.unresolved[i] = ident
			i++
		} else if debugResolve {
			pos := ident.Obj.Decl.(interface{ Pos() token.Pos }).Pos()
			r.trace("resolved %s@%v to package object %v", ident.Name, ident.Pos(), pos)
		}
	}
	file.Scope = r.pkgScope
	file.Unresolved = r.unresolved[0:i]
}

最后得到*ast.File ,可以对其进行后续特定的分析处理

3. AST类型

在 Go 语言中,AST(抽象语法树)主要由三个大类别的节点构成,它们分别是:

基本节点(Basic Nodes):

  • ast.Ident 代表标识符(Identifiers),如变量名、函数名等。
  • ast.BasicLit 代表基本的字面量,如整数、浮点数、字符串等。
  • ast.CompositeLit 代表复合字面量,如数组、切片、字典等。

语句节点(Statement Nodes):

  • ast.ExprStmt 代表一个表达式语句,即只包含一个表达式的语句。
  • ast.AssignStmt 代表赋值语句,包括简单的赋值和多重赋值。
  • ast.IfStmt 代表条件语句。
  • ast.ForStmt 代表循环语句。
  • ast.SwitchStmt 代表开关语句(switch)。
  • ast.RangeStmt 代表 for range 循环语句。

声明节点(Declaration Nodes):

  • ast.GenDecl 代表通用声明,用于表示 importconstvar 等声明。
  • ast.FuncDecl 代表函数声明。
  • ast.TypeSpec 代表类型声明。

所有节点都实现了ast.Node接口,返回了Node的位置信息 

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()
}

以下是ast包中包含的节点类型,可以用到的时候查看官方文档:ast package - go/ast - Go Packages 

 

4. 示例

现在我要获取代码中错误码出现的位置以及对应的名称,下面是源代码

package main

import (
	"errors"
	"log"
)

var DivisionByZero = errors.New("除0错误")
var LessThanZero = errors.New("小于0错误")

func divide(a, b int) (int, error) {
	if b == 0 {
		log.Panic(DivisionByZero)
		return 0, DivisionByZero
	}
	if a < 0 {
		log.Panic(LessThanZero)
		return 0, LessThanZero
	}
	return a / b, nil
}

下面是解析函数: 根据源代码生成的AST节点,遍历一遍获取到需要的声明

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
)

func main() {
	// 文件路径
	path := "example.go"
	// 创建 FileSet
	fset := token.NewFileSet()
	// 解析 Go 代码文件
	f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
	if err != nil {
		fmt.Printf("Error parsing file: %v\n", err)
		os.Exit(1)
	}
	// 在这里可以使用 fset 和 f 进行进一步的操作,例如访问 AST 等
	// Walk through the AST to find error codes
	ast.Inspect(f, func(n ast.Node) bool {
        //表示函数调用表达式。
		if callExpr, ok := n.(*ast.CallExpr); ok {
			if fun, ok := callExpr.Fun.(*ast.SelectorExpr); ok {
				if ident, ok := fun.X.(*ast.Ident); ok {
					if ident.Name == "log" && fun.Sel.Name == "Panic" {
						if len(callExpr.Args) > 0 {
							if arg, ok := callExpr.Args[0].(*ast.Ident); ok {
								fmt.Printf("错误码名称:%s\n", arg.Name)
								fmt.Printf("出现的位置:%s\n", fset.Position(arg.Pos()))
							}
						}
					}
				}
			}
		}
		return true
	})

}

不是很清楚的多debug几遍就大概知道了:

结果如下:

参考资料

https://github.com/chai2010/go-ast-book

ast package - go/ast - Go Packages

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值