GoのASTからソースコードを生成する

Goの標準ライブラリを用いてAST(抽象構文木)からソースコードを生成する方法のメモです。

ASTの取得

文字列からGoのASTを取得する例

import (
  "fmt"
  "go/ast"
  "go/parser"
  "go/token"
)
src := `
package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
}
`
// ソースファイル群を表すデータの作成
// ソースファイルデータにはファイル名やファイル内の構文の位置などの情報を持つ
// たとえばパッケージ単位でコードの解析を行う場合は同一ディレクトリのソースファイルをまとめて扱う必要があるのでソースファイル群という単位でソース情報を持っているものと思われる
fset := token.NewFileSet()
// ソースコードを構文木に変換
// 第二引数にファイル名を渡すとファイルを、第三引数にソースコードの文字列を渡すと文字列を変換する
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
  fmt.Println(err)
  os.Exit(1)
}

ast.Print(fset, f)

go/parserParseFileの戻り値がASTになっている。
go/astPrintを利用して読みやすいように表示させている。

上記プログラムを実行するとこんな感じになる。

     0  *ast.File {
     1  .  Package: ../data/sample.go:1:1
     2  .  Name: *ast.Ident {
     3  .  .  NamePos: ../data/sample.go:1:9
     4  .  .  Name: "main"
     5  .  }
     6  .  Decls: []ast.Decl (len = 2) {
     7  .  .  0: *ast.GenDecl {
     8  .  .  .  TokPos: ../data/sample.go:3:1
     9  .  .  .  Tok: import
    10  .  .  .  Lparen: -
    11  .  .  .  Specs: []ast.Spec (len = 1) {
    12  .  .  .  .  0: *ast.ImportSpec {
    13  .  .  .  .  .  Path: *ast.BasicLit {
    14  .  .  .  .  .  .  ValuePos: ../data/sample.go:3:8
    15  .  .  .  .  .  .  Kind: STRING
    16  .  .  .  .  .  .  Value: "\"fmt\""
    17  .  .  .  .  .  }
    18  .  .  .  .  .  EndPos: -
    19  .  .  .  .  }
    20  .  .  .  }
    21  .  .  .  Rparen: -
    22  .  .  }
    23  .  .  1: *ast.FuncDecl {
    24  .  .  .  Name: *ast.Ident {
    25  .  .  .  .  NamePos: ../data/sample.go:13:6
    26  .  .  .  .  Name: "main"
    27  .  .  .  .  Obj: *ast.Object {
    28  .  .  .  .  .  Kind: func
    29  .  .  .  .  .  Name: "main"
    30  .  .  .  .  .  Decl: *(obj @ 23)
    31  .  .  .  .  }
    32  .  .  .  }
    33  .  .  .  Type: *ast.FuncType {
    34  .  .  .  .  Func: ../data/sample.go:13:1
    35  .  .  .  .  Params: *ast.FieldList {
    36  .  .  .  .  .  Opening: ../data/sample.go:13:10
    37  .  .  .  .  .  Closing: ../data/sample.go:13:11
    38  .  .  .  .  }
    39  .  .  .  }
    40  .  .  .  Body: *ast.BlockStmt {
    41  .  .  .  .  Lbrace: ../data/sample.go:13:13
    42  .  .  .  .  List: []ast.Stmt (len = 1) {
    43  .  .  .  .  .  0: *ast.ExprStmt {
    44  .  .  .  .  .  .  X: *ast.CallExpr {
    45  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {
    46  .  .  .  .  .  .  .  .  X: *ast.Ident {
    47  .  .  .  .  .  .  .  .  .  NamePos: ../data/sample.go:16:2
    48  .  .  .  .  .  .  .  .  .  Name: "fmt"
    49  .  .  .  .  .  .  .  .  }
    50  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    51  .  .  .  .  .  .  .  .  .  NamePos: ../data/sample.go:16:6
    52  .  .  .  .  .  .  .  .  .  Name: "Println"
    53  .  .  .  .  .  .  .  .  }
    54  .  .  .  .  .  .  .  }
    55  .  .  .  .  .  .  .  Lparen: ../data/sample.go:16:13
    56  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    57  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
    58  .  .  .  .  .  .  .  .  .  ValuePos: ../data/sample.go:16:14
    59  .  .  .  .  .  .  .  .  .  Kind: STRING
    60  .  .  .  .  .  .  .  .  .  Value: "\"Hello, World!\""
    61  .  .  .  .  .  .  .  .  }
    62  .  .  .  .  .  .  .  }
    63  .  .  .  .  .  .  .  Ellipsis: -
    64  .  .  .  .  .  .  .  Rparen: ../data/sample.go:16:29
    65  .  .  .  .  .  .  }
    66  .  .  .  .  .  }
    67  .  .  .  .  }
    68  .  .  .  .  Rbrace: ../data/sample.go:17:1
    69  .  .  .  }
    70  .  .  }
    71  .  }
    72  .  Scope: *ast.Scope {
    73  .  .  Objects: map[string]*ast.Object (len = 1) {
    74  .  .  .  "main": *(obj @ 27)
    75  .  .  }
    76  .  }
    77  .  Imports: []*ast.ImportSpec (len = 1) {
    78  .  .  0: *(obj @ 12)
    79  .  }
    80  .  Unresolved: []*ast.Ident (len = 1) {
    81  .  .  0: *(obj @ 46)
    82  .  }
    83  }

ASTからコードを生成

先ほど取得したASTからソースコードを生成する例 とは言ってもめちゃくちゃ簡単。


import (
  "bytes"
  "go/format"
  "go/token"
)

// NodeToCode ...
func NodeToCode(node ast.Node) string {
  buf := new(bytes.Buffer)
  _ = format.Node(buf, token.NewFileSet(), node)
  return buf.String()
}

go/formatNode関数に書き込み先のバッファとFileSet構造体、対象のASTノードを渡してあげるだけ。
これだけでASTからソースコードが生成できる。
上記ではより利用しやすいように関数にしてみた。

上記NodeToCode関数に、先ほど取得したASTノードを渡してみると以下のようなソースコードが得られる。

package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
}

最後に

以前からGoのコード生成は楽だという話を聞いてはいたが衝撃的なくらいに簡単だった。
ASTそのものの編集にはgo/astInspect関数と "golang.org/x/tools/go/ast/astutil"の関数群を利用すればだいぶ楽そうだ。