GeekFactory

int128.hatenablog.com

golang.org/x/tools/go/packages による構文解析と型解析

golang.org/x/tools/go/packages を利用すると,抽象構文木や型情報を利用したコードが簡単に書けるので調べてみました.日本語の情報があまりないようです.

抽象構文木を表示する

Goのソースコードを読み込むには,packages.Load 関数を利用します.packages.Load 関数の引数にはどんな情報を解析してほしいかという設定を渡します.抽象構文木(AST)を取得するには下記のコードのようにフラグを渡します.ドキュメントを読むと packages.NeedSyntax だけで良さそうなのですが,実際に実行してみると複数のフラグが必要でした.

Load 関数の引数にはパッケージ名の配列を渡します../helloworld, os/exec, ./... のような表記が使えます.

package main

import (
    "go/ast"
    "log"
    "os"

    "golang.org/x/tools/go/packages"
)

func main() {
    config := &packages.Config{
        Mode: packages.NeedCompiledGoFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo,
    }
    pkgs, err := packages.Load(config, os.Args[1:]...)
    if err != nil {
        log.Fatalf("could not load packages: %s", err)
    }
    if packages.PrintErrors(pkgs) > 0 {
        log.Fatalf("error occurred")
    }
    for _, pkg := range pkgs {
        if err := ast.Print(pkg.Fset, pkg); err != nil {
            log.Printf("could not print the AST: %s", err)
        }
    }
}

packages.Load 関数はGoのソースファイルの配列を返します.例えば, hello.goworld.go を渡した場合はサイズ2の配列が返ってきます.配列の要素(packages.Package)にはファイル名,抽象構文木,型情報などが含まれています.

先ほどのコードでは ast.Print 関数を利用して抽象構文木をダンプしています.デバッグに便利です.

例えば,以下のソースコードhello.go):

package testdata

import (
    "github.com/pkg/errors"
)

// Hello says hello world!
func Hello() error {
    return errors.New("hello world")
}

に対応する抽象構文木は以下のようになります:

     0  *packages.Package {
     1  .  ID: "github.com/int128/hello-go-parser/testdata"
     2  .  Name: ""
     3  .  PkgPath: ""
     4  .  CompiledGoFiles: []string (len = 2) {
     5  .  .  0: "/src/hello-go-parser/testdata/hello.go"
     6  .  .  1: "/src/hello-go-parser/testdata/world.go"
     7  .  }
     8  .  ExportFile: ""
     9  .  Types: *types.Package {}
    10  .  Fset: *token.FileSet {}
    11  .  IllTyped: false
    12  .  Syntax: []*ast.File (len = 2) {
    13  .  .  0: *ast.File {
    14  .  .  .  Package: /src/hello-go-parser/testdata/hello.go:1:1
    15  .  .  .  Name: *ast.Ident {
    16  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:1:9
    17  .  .  .  .  Name: "testdata"
    18  .  .  .  }
    19  .  .  .  Decls: []ast.Decl (len = 2) {
    20  .  .  .  .  0: *ast.GenDecl {
    21  .  .  .  .  .  TokPos: /src/hello-go-parser/testdata/hello.go:3:1
    22  .  .  .  .  .  Tok: import
    23  .  .  .  .  .  Lparen: /src/hello-go-parser/testdata/hello.go:3:8
    24  .  .  .  .  .  Specs: []ast.Spec (len = 1) {
    25  .  .  .  .  .  .  0: *ast.ImportSpec {
    26  .  .  .  .  .  .  .  Path: *ast.BasicLit {
    27  .  .  .  .  .  .  .  .  ValuePos: /src/hello-go-parser/testdata/hello.go:4:2
    28  .  .  .  .  .  .  .  .  Kind: STRING
    29  .  .  .  .  .  .  .  .  Value: "\"github.com/pkg/errors\""
    30  .  .  .  .  .  .  .  }
    31  .  .  .  .  .  .  .  EndPos: -
    32  .  .  .  .  .  .  }
    33  .  .  .  .  .  }
    34  .  .  .  .  .  Rparen: /src/hello-go-parser/testdata/hello.go:5:1
    35  .  .  .  .  }
    36  .  .  .  .  1: *ast.FuncDecl {
    37  .  .  .  .  .  Doc: *ast.CommentGroup {
    38  .  .  .  .  .  .  List: []*ast.Comment (len = 1) {
    39  .  .  .  .  .  .  .  0: *ast.Comment {
    40  .  .  .  .  .  .  .  .  Slash: /src/hello-go-parser/testdata/hello.go:7:1
    41  .  .  .  .  .  .  .  .  Text: "// Hello says hello world!"
    42  .  .  .  .  .  .  .  }
    43  .  .  .  .  .  .  }
    44  .  .  .  .  .  }
    45  .  .  .  .  .  Name: *ast.Ident {
    46  .  .  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:8:6
    47  .  .  .  .  .  .  Name: "Hello"
    48  .  .  .  .  .  .  Obj: *ast.Object {
    49  .  .  .  .  .  .  .  Kind: func
    50  .  .  .  .  .  .  .  Name: "Hello"
    51  .  .  .  .  .  .  .  Decl: *(obj @ 36)
    52  .  .  .  .  .  .  }
    53  .  .  .  .  .  }
    54  .  .  .  .  .  Type: *ast.FuncType {
    55  .  .  .  .  .  .  Func: /src/hello-go-parser/testdata/hello.go:8:1
    56  .  .  .  .  .  .  Params: *ast.FieldList {
    57  .  .  .  .  .  .  .  Opening: /src/hello-go-parser/testdata/hello.go:8:11
    58  .  .  .  .  .  .  .  Closing: /src/hello-go-parser/testdata/hello.go:8:12
    59  .  .  .  .  .  .  }
    60  .  .  .  .  .  .  Results: *ast.FieldList {
    61  .  .  .  .  .  .  .  Opening: -
    62  .  .  .  .  .  .  .  List: []*ast.Field (len = 1) {
    63  .  .  .  .  .  .  .  .  0: *ast.Field {
    64  .  .  .  .  .  .  .  .  .  Type: *ast.Ident {
    65  .  .  .  .  .  .  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:8:14
    66  .  .  .  .  .  .  .  .  .  .  Name: "error"
    67  .  .  .  .  .  .  .  .  .  }
    68  .  .  .  .  .  .  .  .  }
    69  .  .  .  .  .  .  .  }
    70  .  .  .  .  .  .  .  Closing: -
    71  .  .  .  .  .  .  }
    72  .  .  .  .  .  }
    73  .  .  .  .  .  Body: *ast.BlockStmt {
    74  .  .  .  .  .  .  Lbrace: /src/hello-go-parser/testdata/hello.go:8:20
    75  .  .  .  .  .  .  List: []ast.Stmt (len = 1) {
    76  .  .  .  .  .  .  .  0: *ast.ReturnStmt {
    77  .  .  .  .  .  .  .  .  Return: /src/hello-go-parser/testdata/hello.go:9:2
    78  .  .  .  .  .  .  .  .  Results: []ast.Expr (len = 1) {
    79  .  .  .  .  .  .  .  .  .  0: *ast.CallExpr {
    80  .  .  .  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {
    81  .  .  .  .  .  .  .  .  .  .  .  X: *ast.Ident {
    82  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:9:9
    83  .  .  .  .  .  .  .  .  .  .  .  .  Name: "errors"
    84  .  .  .  .  .  .  .  .  .  .  .  }
    85  .  .  .  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    86  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:9:16
    87  .  .  .  .  .  .  .  .  .  .  .  .  Name: "New"
    88  .  .  .  .  .  .  .  .  .  .  .  }
    89  .  .  .  .  .  .  .  .  .  .  }
    90  .  .  .  .  .  .  .  .  .  .  Lparen: /src/hello-go-parser/testdata/hello.go:9:19
    91  .  .  .  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    92  .  .  .  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
    93  .  .  .  .  .  .  .  .  .  .  .  .  ValuePos: /src/hello-go-parser/testdata/hello.go:9:20
    94  .  .  .  .  .  .  .  .  .  .  .  .  Kind: STRING
    95  .  .  .  .  .  .  .  .  .  .  .  .  Value: "\"hello world\""
    96  .  .  .  .  .  .  .  .  .  .  .  }
    97  .  .  .  .  .  .  .  .  .  .  }
    98  .  .  .  .  .  .  .  .  .  .  Ellipsis: -
    99  .  .  .  .  .  .  .  .  .  .  Rparen: /src/hello-go-parser/testdata/hello.go:9:33
   100  .  .  .  .  .  .  .  .  .  }
   101  .  .  .  .  .  .  .  .  }
   102  .  .  .  .  .  .  .  }
   103  .  .  .  .  .  .  }
   104  .  .  .  .  .  .  Rbrace: /src/hello-go-parser/testdata/hello.go:10:1
   105  .  .  .  .  .  }
   106  .  .  .  .  }
   107  .  .  .  }
   108  .  .  .  Scope: *ast.Scope {
   109  .  .  .  .  Objects: map[string]*ast.Object (len = 1) {
   110  .  .  .  .  .  "Hello": *(obj @ 48)
   111  .  .  .  .  }
   112  .  .  .  }
   113  .  .  .  Imports: []*ast.ImportSpec (len = 1) {
   114  .  .  .  .  0: *(obj @ 25)
   115  .  .  .  }
   116  .  .  .  Unresolved: []*ast.Ident (len = 2) {
   117  .  .  .  .  0: *(obj @ 64)
   118  .  .  .  .  1: *(obj @ 81)
   119  .  .  .  }
   120  .  .  .  Comments: []*ast.CommentGroup (len = 1) {
   121  .  .  .  .  0: *(obj @ 37)
   122  .  .  .  }
   123  .  .  }
   124  .  .  1: *ast.File {
(中略)
   312  .  .  }
   313  .  }
   314  }

抽象構文木を探索する

抽象構文木を探索するには ast.Inspect を利用します.コールバック関数に抽象構文木のノードが渡されるので,ノードの型をチェックして必要な処理を行います.

例えば,以下を実行するとソースコードに含まれる import 文を抽出できます.

   for _, pkg := range pkgs {
        for _, syntax := range pkg.Syntax {
            ast.Inspect(syntax, func(node ast.Node) bool {
                switch node := node.(type) {
                case *ast.ImportSpec:
                    log.Printf("import %s as %s", node.Path.Value, node.Name)
                }
                return true
            })
        }
    }
2019/09/03 18:06:32 import "github.com/pkg/errors" as <nil>

また,以下を実行すると関数やメソッドの呼び出しを抽出できます.

   for _, pkg := range pkgs {
        for _, syntax := range pkg.Syntax {
            ast.Inspect(syntax, func(node ast.Node) bool {
                switch node := node.(type) {
                case *ast.CallExpr:
                    switch fun := node.Fun.(type) {
                    // foo.bar() 形式の呼び出し
                    case *ast.SelectorExpr:
                        log.Printf("call %s.%s with %d arg(s)", fun.X, fun.Sel, len(node.Args))
                    // foo() 形式の呼び出し
                    default:
                        log.Printf("call %s with %d arg(s)", fun, len(node.Args))
                    }
                }
                return true
            })
        }
    }
2019/09/03 19:09:59 call errors.New with 1 arg(s)

型情報を利用する

ast.Inspect 関数で得られるノードには型情報が含まれないため,関数やメソッドの呼び出し対象を判断するのが難しい問題があります.関数やメソッドの呼び出し対象は以下の種類があります.

import "fmt"

// 同じパッケージの関数
hello()

// 外部のパッケージの関数
fmt.Printf("hello world")

// メソッド
var x Hello
x.Hello()

そこで, packages.Load 関数が返す TypesInfo を利用すると,ノードの型を取得できます.例えば,以下を実行すると外部パッケージの関数に対する呼び出しを取得できます.

   for _, pkg := range pkgs {
        for _, syntax := range pkg.Syntax {
            ast.Inspect(syntax, func(node ast.Node) bool {
                switch node := node.(type) {
                case *ast.CallExpr:
                    switch fun := node.Fun.(type) {
                    // foo.bar() 形式の呼び出し
                    case *ast.SelectorExpr:
                        switch x := fun.X.(type) {
                        case *ast.Ident:
                            switch o := pkg.TypesInfo.ObjectOf(x).(type) {
                            // fooがパッケージの場合
                            case *types.PkgName:
                                // パッケージ名を取得
                                path := o.Imported().Path()
                                log.Printf("call %s.%s with %d arg(s)", path, fun.Sel, len(node.Args))
                            }
                        }
                    }
                }
                return true
            })
        }
    }
2019/09/03 19:53:03 call github.com/pkg/errors.New with 1 arg(s)

ここでは関数呼び出しのパッケージ名を取得しましたが,変数の型なども取得できます.

まとめ

golang.org/x/tools/go/packages を利用すると,抽象構文木や型情報を利用するコードが簡単に書けます.