Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rewrite Go error handling using AST transformation

Rewrite Go error handling using AST transformation

golang.tokyo #28
2019.12.4

Hidetake Iwata

December 04, 2019
Tweet

More Decks by Hidetake Iwata

Other Decks in Programming

Transcript

  1. Rewrite Go error handling
    using AST transformation
    golang.tokyo #28
    Hidetake Iwata (@int128)

    View Slide

  2. 岩田 英丈 / Hidetake Iwata
    2
    Software Engineer at NTT DATA (Senior YAML/Terraform Engineer )
    Open Source Developer at https://github.com/int128

    View Slide

  3. Agenda
    3
    お話しすること
    ● Goにおける抽象構文木の変換と操作
    ● アプリケーションのエラー処理を書き換える方法
    お話ししないこと
    ● Goにおけるエラー処理の詳細

    View Slide

  4. Error handling in Go
    Goでエラー処理を行うには主に以下の方法がある
    ● errorsパッケージを利用する(Go 1.13で拡張された)
    ● golang.org/x/xerrors パッケージを利用する
    ● github.com/pkg/errors パッケージを利用する
    他にもサードパーティのパッケージが公開されている
    (e.g. github.com/morikuni/failure)
    4

    View Slide

  5. 以下のような目的で、アプリケーション内のエラー処理を別の方法に書き換えたいこ
    とがある
    ● 特定の機能を使いたい(e.g. エラー型判定、スタックトレース)
    ● 外部パッケージを使いたくない
    ● ソースコードの可読性を改善したい
    Rewrite the Go error handling
    5

    View Slide

  6. 例:pkg/errors.Wrapf() を xerrors.Errorf() に
    書き換える
    ● import文を変更する
    ● 関数名を変更する
    ● 引数の順序を入れ替える
    ● 引数のフォーマット文字列に : %w を付け加える
    6
    // pkg/errorsの場合
    return nil, errors.Wrapf(err, "item id=%s not found", id)
    // xerrorsの場合
    return nil, xerrors.Errorf("item id=%s not found: %w", id, err)

    View Slide

  7. 簡単な書き換えは正規表現による一括置換でできるが、
    複雑な書き換えには以下の手法が広く利用されている
    1. ソースコードを抽象構文木に変換する
    2. 抽象構文木を操作する
    3. 抽象構文木をソースコードに書き出す
    @tenntennさんのスライドが分かりやすいです
    https://www.slideshare.net/takuyaueda967/go-74970321
    抽象構文木の操作による書き換え
    7

    View Slide

  8. golang.org/x/tools/go/packages パッケージ等を利用すると、ソースコードと抽象
    構文木を相互に変換できる
    Goのソースコードと抽象構文木の相互変換
    https://godoc.org/golang.org/x/tools/go/packages
    cfg := &packages.Config{/* … */}
    pkgs, err := packages.Load(cfg, pkgNames...) // ソースコードを解析する
    if err != nil {/* … */}
    if packages.PrintErrors(pkgs) > 0 {/* … */}
    for _, pkg := range pkgs {
    for _, file := range pkg.Syntax {
    err := printer.Fprint(os.Stdout, pkg.Fset, file) // 抽象構文木を出力する
    }
    }
    8

    View Slide

  9. 0 *packages.Package {
    1 . ID: "_/hello-go-ast-transformation"
    2 . Name: ""
    3 . PkgPath: ""
    4 . CompiledGoFiles: []string (len = 1) {
    5 . . 0: "/hello-go-ast-transformation/main.go"
    6 . }
    7 . ExportFile: ""
    8 . Types: *types.Package {}
    9 . Fset: *token.FileSet {}
    10 . IllTyped: false
    11 . Syntax: []*ast.File (len = 1) {
    12 . . 0: *ast.File {
    13 . . . Package: /hello-go-ast-transformation/main.go:1:1
    14 . . . Name: *ast.Ident {
    15 . . . . NamePos: /hello-go-ast-transformation/main.go:1:9
    16 . . . . Name: "main"
    17 . . . }
    18 . . . Decls: []ast.Decl (len = 3) {
    19 . . . . 0: *ast.GenDecl {
    20 . . . . . TokPos: /hello-go-ast-transformation/main.go:3:1
    21 . . . . . Tok: import
    22 . . . . . Lparen: /hello-go-ast-transformation/main.go:3:8
    23 . . . . . Specs: []ast.Spec (len = 5) {
    24 . . . . . . 0: *ast.ImportSpec {
    25 . . . . . . . Path: *ast.BasicLit {
    packages.Load() が返す抽象構文木の基本構造
    パッケージ
    ファイル
    ファイル
    import宣言
    関数宣言


    https://github.com/int128/hello-go-ast-transformation
    9

    View Slide

  10. 148 1: *ast.CallExpr {
    149 . Fun: *ast.SelectorExpr {
    150 . . X: *ast.Ident {
    151 . . . NamePos: hello.go:12:14
    152 . . . Name: "errors"
    153 . . }
    154 . . Sel: *ast.Ident {
    155 . . . NamePos: hello.go:12:21
    156 . . . Name: "Wrapf"
    157 . . }
    158 . }
    159 . Lparen: hello.go:12:26
    160 . Args: []ast.Expr (len = 3) {
    161 . . 0: *ast.Ident {
    162 . . . NamePos: hello.go:12:27
    163 . . . Name: "err"
    164 . . . Obj: *(obj @ 107)
    165 . . }
    166 . . 1: *ast.BasicLit {
    167 . . . ValuePos: hello.go:12:32
    168 . . . Kind: STRING
    169 . . . Value: "\"item id=%s not found\""
    170 . . }
    171 . . 2: *ast.Ident {
    172 . . . NamePos: hello.go:12:56
    173 . . . Name: "id"
    174 . . . Obj: *(obj @ 33)
    175 . . }
    176 . }
    errors.Wrapf(err, "item id=%s not found", id) に対応する部分木
    CallExpr
    Selector
    Expr
    Ident
    “errors”
    Ident
    “Wrapf”
    Ident
    “err”
    BasicLit
    “item...”
    Ident
    “id”
    []Expr
    10

    View Slide

  11. 156 1: *ast.CallExpr {
    157 . Fun: *ast.SelectorExpr {
    158 . . X: *ast.Ident {
    159 . . . NamePos: hello.go:13:14
    160 . . . Name: "xerrors"
    161 . . }
    162 . . Sel: *ast.Ident {
    163 . . . NamePos: hello.go:13:22
    164 . . . Name: "Errorf"
    165 . . }
    166 . }
    167 . Lparen: hello.go:13:28
    168 . Args: []ast.Expr (len = 3) {
    169 . . 0: *ast.BasicLit {
    170 . . . ValuePos: hello.go:13:29
    171 . . . Kind: STRING
    172 . . . Value: "\"item id=%s not found: %w\""
    173 . . }
    174 . . 1: *ast.Ident {
    175 . . . NamePos: hello.go:13:57
    176 . . . Name: "id"
    177 . . . Obj: *(obj @ 41)
    178 . . }
    179 . . 2: *ast.Ident {
    180 . . . NamePos: hello.go:13:61
    181 . . . Name: "err"
    182 . . . Obj: *(obj @ 115)
    183 . . }
    184 . }
    xerrors.Errorf("item id=%s not found: %w", id, err) に対応する部分木
    CallExpr
    Selector
    Expr
    Ident
    “xerrors”
    Ident
    “Errorf”
    Ident
    “err”
    BasicLit
    “item...”
    Ident
    “id”
    []Expr
    11

    View Slide

  12. pkg/errors.Wrapf() を xerrors.Errorf() に書き換える = 抽象構文木を操作する
    CallExpr
    Selector
    Expr
    Ident
    “errors”
    Ident
    “Wrapf”
    Ident
    “err”
    BasicLit
    “item...”
    Ident
    “id”
    []Expr
    CallExpr
    Selector
    Expr
    Ident
    “xerrors”
    Ident
    “Errorf”
    Ident
    “err”
    BasicLit
    “item...”
    Ident
    “id”
    []Expr
    pkg/errors.Wrapf() の部分木 xerrors.Errorf() の部分木
    12

    View Slide

  13. 抽象構文木の探索
    go/astパッケージの Inspect() や Walk() を利用すると、抽象構文木を深さ優先探索
    できる
    ast.Inspect(file, func(node ast.Node) bool {
    switch node := node.(type) {
    case *ast.CallExpr: // 関数呼び出しの場合
    switch fun := node.Fun.(type) {
    case *ast.SelectorExpr:
    switch x := fun.X.(type) {
    case *ast.Ident:
    if x.Sel.Name == "errors" { // パッケージ名がerrorsの場合
    x.Sel.Name = "xerrors" // パッケージ名を書き換える
    }
    13
    https://golang.org/pkg/go/ast/#Inspect

    View Slide

  14. 関数呼び出しの型解決
    抽象構文木のノードには型情報が含まれないので、
    そのままでは変数や関数呼び出しの実体を判断できない
    例:ソースコードに書かれているerrorsの実体はどれでしょう?
    ● 標準パッケージ
    ● 別名インポートされたパッケージ?
    ● パッケージスコープのerrors変数?
    14
    func HelloWorld() error {
    return errors.New("hello")
    }

    View Slide

  15. go/typesによる型解決
    15
    go/typesパッケージの Info.ObjectOf() で識別子の型を解決できる
    ast.Inspect(file, func(node ast.Node) bool {
    switch node := node.(type) {
    case *ast.CallExpr: // 関数呼び出しの場合
    switch fun := node.Fun.(type) {
    case *ast.SelectorExpr:
    switch x := fun.X.(type) {
    case *ast.Ident:
    switch o := pkg.TypesInfo.ObjectOf(x).(type) {
    case *types.PkgName: // 関数呼び出しの左辺がパッケージの場合
    pkgPath := o.Imported().Path()
    https://golang.org/pkg/go/types/#Info.ObjectOf

    View Slide

  16. まとめ
    Goにおける抽象構文木の変換と操作を用いて、アプリケーションのエラー処理を書
    き換える方法を紹介しました
    アプリケーションのエラー処理を書き換えるツールを作っているので、
    よかったらフィードバックをお願いします!!
    ✨ https://github.com/int128/transerr
    16

    View Slide

  17. We are hiring!
    www.nttdata-careers.com
    17

    View Slide