美文网首页
GO/ast编程(三)-ast生成代码

GO/ast编程(三)-ast生成代码

作者: 温岭夹糕 | 来源:发表于2024-01-13 10:56 被阅读0次

目录

链接地址

1.常用节点学习

1.1Decl节点

demo1

func TestAstbuild(t *testing.T) {
    str :=
        `//@auther zjb
    package ast
    
    //@hi
    func main(){
    
    }
`
    src := []byte(str)
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "src.go", src, parser.ParseComments)
    if err != nil {
        t.Fatal(err)
    }
    var v AstVistor
    v.Com = make(map[string]string)

    ast.Inspect(f, func(n ast.Node) bool {
    //断点打在这里,不断查看n的类型和值
        return true
    })
}
ast.Inspect是ast.Walk的封装,内部都是使用深度优先遍历DFS,因此我们可以通过打断点的方式绘制出该ast结构 image.png 其中FuncDecl还可以继续展开成更复杂的形式 image.png
还记得前文提到的Decl是什么节点吗?是声明节点,那么这个FuncDecl是函数声明的节点,实际上,Decl声明节点共有三种:
  1. FuncDecl
  2. BadDecl 语法错误声明的占位符节点
  3. GenDecl 通用声明节点(包括import导入、const常量、type类型、variable变量),那么这里type可以是声明一个普通类型
type a int

也可以是声明一个接口或结构体类型

type a interface{}

为了验证我们的猜想,我们将上述demo变量修改

    str :=
        `
package ast

type a interface{}
`
经过打断点发现确实如此(实际上也是如此) image.png

在GenDecl源码的注释中描述了以下3种声明类型

    //  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, or VAR
        Lparen token.Pos     // position of '(', if any
        Specs  []Spec
        Rparen token.Pos // position of ')', if any
    }

那么如果有“通过接口定义来生成代码”这样的需求,我们就可以使用GenDecl来定位声明接口的位置

1.2 Spec节点类型

Spec在前文ast.File字段的学习中我们了解过(ImportSpec是导入声明),我们在上文1.1学习到Spec接口是和GenDecl通用声明节点是强关联的(是以切片形式被保存在GenDecl.Spec中),其他两个很容易理解,这里主要看TypeSpec

    TypeSpec struct {
        Doc        *CommentGroup // associated documentation; or nil
        Name       *Ident        // type name
        TypeParams *FieldList    // type parameters; or nil
        Assign     token.Pos     // position of '=', if any
        Type       Expr          // *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes
        Comment    *CommentGroup // line comments; or nil
    }

我们看到TypeSpec里面的Type是一个表达式节点,它可以是:

  1. Ident 标识符表达式节点
  2. ParenExpr 括号表达式节点
  3. SelectorExpr 选择器表达式节点
  4. StarExpr 索引表达式节点
  5. XxxTypes 包括ArrayType,structType,FuncType,InterfacType,MapType,ChanType ,即可以和type关键字一起使用的那些

2.实战简单代码生成

2.1定位接口声明

myterface.go

package myast

type RouteAble interface {}

demo2

func TestMakeAst(t *testing.T) {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "./myinterface.go", nil, 0)
    if err != nil {
        t.Fatal(err)
    }
    ast.Inspect(f, func(n ast.Node) bool {
        if genNode, ok := n.(*ast.GenDecl); ok {
            for _, spec := range genNode.Specs {
                if tp_spec, ok := spec.(*ast.TypeSpec); ok {
                    if _, ok := tp_spec.Type.(*ast.InterfaceType); ok {
                        t.Log(tp_spec.Name.Name)//TODO 代码生成
                    }
                }
            }
        }
        return true
    })
}

//output:
// RouteAble

实际上就是遍历ast并进行类型断言,有点前端js回调地狱的那味道了。

2.2手动构建ast

那既然定位好了,接口的信息也都能获取了,接下来就是要生成对应实现了,我们回想一下第一篇文章提到的gofmt核心是把源代码变成ast,再由ast生成代码,意味着我们也要构建对应的ast,那肯定不能在当前树操作

    out,err := parser.ParseFile(fset,"out.go","package myast",0)
    if err != nil {
        t.Fatal(err)
    }

接口生成对应的实现也应该是一个声明,它应该是一个*ast.TypeSpec,且字段Type是structType

                        structName := tp_spec.Name.Name+"Default"
                        new_typeSpec := &ast.TypeSpec{
                            Name: ast.NewIdent(structName),
                            Type: &ast.StructType{
                                Fields: &ast.FieldList{},
                            },
                        }

注意这里StructType.Fileds表示结构体成员,我们默认没有

    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
    }

type FieldList struct {
    Opening token.Pos //  左括号位置
    List    []*Field  // field list; or nil
    Closing token.Pos // 右括号位置
}

然后我们创建的TypeSpec需要一个GenDecl载体并挂载上去,同时GenDecl也要挂载到根节点ast.File上

                        new_decl := &ast.GenDecl{
                            Tok: token.TYPE,
                            Specs: []ast.Spec{new_typeSpec},
                        }
                        out.Decls = append(out.Decls, new_decl)

此时代码为

func TestMakeAst(t *testing.T) {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "./myinterface.go", nil, 0)
    if err != nil {
        t.Fatal(err)
    }
    out,err := parser.ParseFile(fset,"out.go","package myast",0)
    if err != nil {
        t.Fatal(err)
    }
    ast.Inspect(f, func(n ast.Node) bool {
        if genNode, ok := n.(*ast.GenDecl); ok {
            for _, spec := range genNode.Specs {
                if tp_spec, ok := spec.(*ast.TypeSpec); ok {
                    if _, ok := tp_spec.Type.(*ast.InterfaceType); ok {
                        //TODO
                        structName := tp_spec.Name.Name+"Default"
                        new_typeSpec := &ast.TypeSpec{
                            Name: ast.NewIdent(structName),
                            Type: &ast.StructType{
                                Fields: &ast.FieldList{},
                            },
                        }
                        new_decl := &ast.GenDecl{
                            Tok: token.TYPE,
                            Specs: []ast.Spec{new_typeSpec},
                        }
                        out.Decls = append(out.Decls, new_decl)
                    }
                }
            }
        }
        return true
    })
}

2.3生成代码

使用go/format来翻译ast树成字节流,这里使用format.Node

    file, err := os.Create("out.go")
    if err != nil {
        t.Fatal(err)
    }
    defer file.Close()

    var buf bytes.Buffer
    err = format.Node(&buf, fset, out)
    if err != nil {
        t.Fatal(err)
    }
    _, _ = file.WriteString(buf.String())
效果如下 image.png
那么问题来了,函数怎么办,也就是最复杂的FuncDecl,而且节点类型还有stmt语句节点还没解析,那是不是两者有一些关系?我们回头看开头demo画的ast image.png ast.FuncDecl下有一个ast.BlockStmt,两者之间确实有联系

3.函数

3.1InterfaceType

    // An InterfaceType node represents an interface type.
    InterfaceType struct {
        Interface  token.Pos  // position of "interface" keyword
        Methods    *FieldList // list of embedded interfaces, methods, or types
        Incomplete bool       // true if (source) methods or types are missing in the Methods list
    }
  • InterfaceType.Methods 是一个列表或集合,可以是内嵌的接口、类型或方法,这很容易理解,GO不支持继承,使用都是嵌入,FildList一些Expr表达式节点的装饰集合
type FieldList struct {
    Opening token.Pos //  左括号位置
    List    []*Field  // field list; or nil
    Closing token.Pos // 右括号位置
}
type Field struct {
    Doc     *CommentGroup // associated documentation; or nil
    Names   []*Ident      // field/method/(type) parameter names; or nil
    Type    Expr          // field/method/parameter type; or nil
    Tag     *BasicLit     // field tag; or nil
    Comment *CommentGroup // line comments; or nil
}
  • 需要注意Field.Names是一个切片,如果她是一个方法节点,即Field.Type== *ast.FuncType,那么切片中只有函数名一个值
  • InterfaceType.Incomplete 如果没有方法或类型,即空接口eface,那么则为true

那么我们就可以利用methods字段提取出接口的方法了,我们给我们的接口添加方法

//myinterface.go
type RouteAble interface {
    Route(method, path string) string
}

获取接口方法demo2.1

//实战代码片段 inf 是通过断言*ast.interfaceType 获得的变量
                        if !inf.Incomplete {
                            for _, method := range inf.Methods.List {
                                //如果Interface.Methods.List中获取的Filed不是方法
                                //则暂时不处理
                                _, ok := method.Type.(*ast.FuncType)
                                if !ok {
                                    continue
                                }

                                var method_name string
                                for _, name := range method.Names {
                                    method_name = name.Name
                                }
                                if method_name == "" {
                                    panic("no method name found")
                                }
                                t.Log(method_name)

                            }                       
//output:Route

3.2FuncType

表达式节点FuncType 代表了一整个函数

func TestXxx(t string) string 

源码

    FuncType struct {
        Func       token.Pos  // position of "func" keyword (token.NoPos if there is no "func")
        TypeParams *FieldList // type parameters; or nil
        Params     *FieldList // (incoming) parameters; non-nil
        Results    *FieldList // (outgoing) results; or nil
    }

params和Results顾名思义表示参数和返回值

3.3FuncDecl

我们想要生成的ast是什么样的,是不是类似下面代码的ast?
demo3

package myast

type H struct {
}

func (h *H) Hello(a string) string {
    return "hello"
}
我们通过ast.Inspect和打断点画出ast.FuncDecl分支 image.png

观察到它有一个方法接收者、函数名、参数、返回类型、函数体和返回值这些要素,我们构建FuncDecl也需要这些,查看FuncDecl源码

    FuncDecl struct {
        Doc  *CommentGroup // associated documentation; or nil
        Recv *FieldList    // receiver (methods); or nil (functions)
        Name *Ident        // function/method name
        Type *FuncType     // function signature: type and value parameters, results, and position of "func" keyword
        Body *BlockStmt    // function body; or nil for external (non-Go) function
    }
  • Recv是方法的接收者,如果不是结构体的函数,则为nil;如果是则它的Field.Names包含接受指针名,Field.Type==ast.StarExpr,即下面这个分支 image.png
  • Name函数名属性
  • Type FuncType见上一节3.2中分析
  • Body 是BlockStmt类型,stmt是一个接口,函数体里面有哪些句子就往里面塞各自的语句节点,如for循环就塞forstmt,if就塞ifstmt
    BlockStmt struct {
        Lbrace token.Pos // position of "{"
        List   []Stmt
        Rbrace token.Pos // position of "}", if any (may be absent due to syntax error)
    }
demo3只有返回语句 image.png
    ReturnStmt struct {
        Return  token.Pos // position of "return" keyword
        Results []Expr    // result expressions; or nil
    }

我们看到ReturnStmt.Results是切片类型,这也是GO能实现多返回值的条件之一(主要还是函数栈)

3.4构建我们自己的FuncDecl

又要返回demo2.1继续扩展,注意FiledList不要为nil,需要初始化
demo2.2

                        out.Decls = append(out.Decls, new_decl)
                        //先不支持内嵌 获取接口方法
                        if !inf.Incomplete {
                            for _, method := range inf.Methods.List {
                                //如果Interface.Methods.List中获取的Filed不是方法
                                //则暂时不处理
                                method_type, ok := method.Type.(*ast.FuncType)
                                if !ok {
                                    continue
                                }

                                var method_name string
                                for _, name := range method.Names {
                                    method_name = name.Name
                                }
                                if method_name == "" {
                                    panic("no method name found")
                                }
                                //方法接收者是存储在FuncDecl的FieldList类型中
                                f_recv := &ast.FieldList{
                                    List: []*ast.Field{
                                        {
                                            // 这里我们取接口的首字母小写
                                            Names: []*ast.Ident{ast.NewIdent(strings.ToLower(string(structName[0])))},
                                            Type:  &ast.StarExpr{X: ast.NewIdent(structName)},
                                        },
                                    },
                                }

                                //方法参数存储在FuncDecl的FuncType的FieldList中
                                f_type := &ast.FuncType{
                                    Params: &ast.FieldList{
                                        List: method_type.Params.List,
                                    },
                                    Results: &ast.FieldList{
                                        List: method_type.Results.List,
                                    },
                                }
                                //给每个返回类型创建默认返回值
                                var res []ast.Expr
                                for _, field := range method_type.Results.List {
                                    switch ident := reflect.ValueOf(field.Type).Elem().Interface().(type) {
                                    case ast.Ident:
                                        res = append(res, CreateZeroExpr(ident.Name))
                                    default:
                                        res = append(res, &ast.Ident{Name: "nil"})
                                    }
                                }
                                //方法的函数体
                                f_body := &ast.BlockStmt{
                                    List: []ast.Stmt{
                                        &ast.ReturnStmt{Results: res},
                                    },
                                }

                                //注解
                                f_comment := &ast.CommentGroup{
                                    List: []*ast.Comment{
                                        {
                                            Text: "//@auther xxxx",
                                        },
                                        {
                                            Text: "//TODO",
                                        },
                                    },
                                }

                                //组装
                                new_funcDecl := &ast.FuncDecl{
                                    Name: ast.NewIdent(method_name),
                                    Doc:  f_comment,
                                    Recv: f_recv,
                                    Type: f_type,
                                    Body: f_body,
                                }
                                //添加到根节点下
                                out.Decls = append(out.Decls, new_funcDecl)
                            }
func CreateZeroExpr(name string) ast.Expr {
    switch name {
    case "bool":
        return &ast.Ident{Name: "false"}
    case "int","uint":
        return &ast.BasicLit{Kind: token.INT, Value: "0"}
    case "float32", "float64":
        return &ast.BasicLit{Kind: token.FLOAT, Value: "0.0"}
    case "string":
        return &ast.BasicLit{Kind: token.STRING, Value: `""`}
    default:
        return &ast.Ident{Name: "nil"}
    }
}
效果如下 image.png

需要注意如果接口有包导入,我们这里也要添加,但demo并未写出,需要自行添加,至于怎么加,自己分析ast结构照猫画虎去吧(需要注意import也是ast.GenDecl类型)

至此我们用3节内容已经基本掌握了ast编程,最后一节把编译内容前端的语法分析和后端部分补全

补充

1.包导入

// 解析import
func parseImports(file, out *ast.File) {
    import_decl := &ast.GenDecl{
        Tok:   token.IMPORT,
        Specs: []ast.Spec{},
    }
    for _, imp := range file.Imports {
        new_imp := &ast.ImportSpec{
            Path: &ast.BasicLit{
                Kind:  token.STRING,
                Value: imp.Path.Value,
            },
        }
        import_decl.Specs = append(import_decl.Specs, new_imp)
    }
    out.Decls = append(out.Decls, import_decl)
}

参考

ast编程
每日一库之ast

相关文章

  • Golang标准库——go(1)

    ast ast ast包声明用于表示Go包语法树的类型。 func FileExports FileExports...

  • vue源码目录设计

    compiler 包含vue.js所有编译相关代码。包括把模版解析成ast树,ast语法树优化,代码生成等工具。 ...

  • AST与babel

    AST 什么是ast ast又称抽象语法树,以树形结构描述代码,一般是json对象。与ast相关的操作通常有三...

  • 编译原理术语解释

    编译流程 词法分析(生成token流)->语法分析(生成AST)→语义分析(AST)→生成字节码(生成IR ,可跳...

  • 使用typescript来修改js文件

    原理 使用typescript把源代码读成ast,然后修改ast结构,然后将ast重新还原成js代码,最后将js代...

  • 代码混淆

    LLVM编译过程: 预处理,词法分析,token,语法分析,AST,代码生成,LLVM IR,优化,生成,汇编代码...

  • babel 与 AST

    babel原理 parse把代码code 变成 ast traverse 遍历ast 进行修改 generate ...

  • Go ast

    AST是抽象语法树(Abstract Syntax Tree)的简称,AST以树状形式表现编程语言的语法结构,树上...

  • AST

    什么是ast ast就是把一段代码,然后转换成一颗语法树。 ast 如何工作 1, 词法分析 2,句法分析 ast...

  • Swift day-1

    生成语法树:swiftc-dump-ast main.swift 生成最简洁的SIL代码:swiftc -emit...

网友评论

      本文标题:GO/ast编程(三)-ast生成代码

      本文链接:https://www.haomeiwen.com/subject/henzgdtx.html