Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

静的解析を使った開発ツールの開発

2,410 views

Published on

kamakura.go#3で発表した資料です。
https://connpass.com/event/74421/

Published in: Technology
  • Be the first to comment

静的解析を使った開発ツールの開発

  1. 1. The Go gopher was designed by Renée French. The gopher stickers was made by Takuya Ueda. Licensed under the Creative Commons 3.0 Attributions license. 静的解析を使った 開発ツールの開発 @kamakura.go #3 2018年1月20日(土)
  2. 2. 自己紹介 上田拓也 @tenntenn 所属 コミュニティ活動 & Go ビギナーズ Go Conference 上田拓也 @tenntenn 2
  3. 3. ソウゾウ エキスパートチーム 技術をアウトプットするところに技術は集まる ■ エキスパートチームとは? ● 50%以上の時間を技術コミュニティへの貢献に充てる ■ エキスパートチームの役割 ● 社内に新しい技術を取り取り込む ● 社外のコミュニティなどを通じて社会へ還元する ■ エキスパートチームの活動 ● カンファレンス・勉強会の開催/運営 ● 対外的な講演活動 ● 執筆、雑誌への寄稿、インタビュー ● 社内外での担当技術の普及推進 @tenntenn 担当:Go・GCP @mhidaka 担当:Android メンバー 3
  4. 4. アジェンダ ■ Gopherを探せ! ■ 静的解析と開発ツール ■ Goにおける静的解析 〜基礎編〜 ■ Goにおける静的解析 〜応用編〜 4
  5. 5. Gopherを探せ! みつけてね Powered by https://gopherize.me 5
  6. 6. Gopherを探せ! みつけたかな? Powered by https://gopherize.me 6
  7. 7. "Gopher"を探せ! type Gopher struct { Gopher string `json:"gopher"` } func main() { const gopher = "GOPHER" gogopher := GOPHER() gogopher.Gopher = gopher fmt.Println(gogopher) } func GOPHER() (gopher *Gopher) { gopher = &Gopher{ Gopher: "gopher" } return } 7
  8. 8. みんな大好きgrep $ grep Gopher main.go type Gopher struct { Gopher string `json:"gopher"` } gogopher.Gopher = gopher func GOPHER() (gopher *Gopher) { gopher = &Gopher{ Gopher: "gopher" } grepで"Gopher"を探す 8
  9. 9. "Gopher" 型を探せ! type Gopher struct { Gopher string `json:"gopher"` } func main() { const gopher = "GOPHER" gogopher := GOPHER() gogopher.Gopher = gopher fmt.Println(gogopher) } func GOPHER() (gopher *Gopher) { gopher = &Gopher{ Gopher: "gopher" } return } 9
  10. 10. "Gopher"型を探すには? ■ grep だと文字列としてしか検索できない ■ 文字列ではなくGoのソースコードとして 理解しないといけない ■ そこで静的解析を使う! 10
  11. 11. 静的解析と開発ツール 11
  12. 12. 静的解析と動的解析 ■ 静的解析 ● プログラムを実行せずに解析すること ● ソースコードを解析する ● 例:lint、コード補完、 コードフォーマッタ ■ 動的解析 ● プログラムを実行して解析すること ● 実行時 変数 状態や関数 実行順などを検証 ● 例:レースコンディション 12
  13. 13. リフレクション ■ リフレクション ● 実行時に型や値 情報を解析する ■ Goにおけるリフレクション ● reflectパッケージを使う ● 実行時にstructタグにアクセスする唯一の方法 ● 出力引数としてポインタを貰ってきた場合に値を入れるために使用 ● JSONやXMLなどのエンコードに使用 13
  14. 14. Goと静的解析 ■ 静的解析しやすいように設計されている ● 静的型付け ● 文法がシンプル ● 型推論 ● 暗黙の型変換がない ● 標準パッケージで静的解析の機能を提供 静的解析では多くの情報を取得できる 14
  15. 15. go パッケージ go/ast 抽象構文木(AST)を提供 go/build パッケージに関する情報を集める go/constant 定数に関する型を提供 go/doc ドキュメントをASTから取り出す go/format コードフォーマッタ機能を提供 go/importer コンパイラに適したImporterを提供 go/parser 構文解析 機能を提供 go/printer AST 表示機能を提供 go/scanner 字句解析 機能を提供 go/token トークンに関する型を提供 go/types 型チェックに関する機能を提供 15
  16. 16. Goの静的解析ツール ■ Goには多くの静的解析ツールが存在する gofmt/goimports コードフォーマッタ go vet/golint コードチェッカー、リンター guru 静的解析 gocode コード補完 errcheck エラー処理のチェック gorename/gomvpkg リファクタリング 16
  17. 17. 開発ツールへ静的解析を使う ■ 自作開発ツールでレビューコストを下げる ● 自動化できることは自動化する(カラクリ) ○ 自分のプロジェクト用にカスタマイズされたリンターを作る ○ よくあるバグを検出するツールを作る ● レビューの時間はもっと重要なことに使う ○ アルゴリズムの検証 ○ 設計 ○ パフォーマンス ■ コードやドキュメントを自動生成する ● マイグレーション ● ドキュメント生成 17
  18. 18. Goにおける静的解析 〜基礎編〜 18
  19. 19. 静的解析の流れ ソースコード トークン 抽象構文木 (AST) 型情報 構文解析 字句解析 型チェック go/scanner go/token go/parser go/ast go/types go/constant 19
  20. 20. ■ 入力された文字列をトークンとして分解 字句解析- go/scanner,go/token IDENT ADD INT トークン ソースコード: v + 1 20
  21. 21. 構文解析 - go/parser,go/ast ■ トークンを抽象構文木(AST)に変換 v + 1 IDENT ADD INT ソースコード: + v 1 BinaryExpr Ident BasicLit トークン: 抽象構文(AST): 21
  22. 22. "Hello, World"の抽象構文木 package main import "fmt" func main() { fmt.Println("Hello, 世界") } Run on Playground *ast.File []ast.Decl *ast.GenDecl *ast.FuncDecl 22
  23. 23. 型チェック - go/types,go/constant ■ 型情報を抽象構文木から抽出 ● 識別子の解決 ● 型の推論 ● 定数の評価 n := 100 + 200 m := n + 300 定数の評価 = 300 型の推論 -> int 識別子の解決 23
  24. 24. ソースコードから抽象構文木の取得 ■ parser.Parse* 系の関数を使う ● ParseExpr,ParseExprFrom ○ 式をパースする ○ ParseExpr は ParseExprFrom のシンプルバージョン ● ParseFile ○ ファイルをパースする ● ParseDir ○ ディレクトリ単位でパースする ○ 内部で ParseFile を呼んでいる 24
  25. 25. 式の抽象構文木を取得する expr, err := parser.ParseExpr(`v + 1`) if err != nil { /* handling the error */ } /* use expr */ ■ 式単位でパースする 25
  26. 26. ファイル単位で抽象構文木を取得 const src = ` package main var v = 100 func main() { fmt.Println(v+1) }` fs := token.NewFileSet() f, err := parser.ParseFile(fs, "my.go", src, 0) if err != nil { /* handling the error */ } /* use f */ srcがnilの場合だと ファイル名を元にファイルが開かれる ソースコード パースモード 26
  27. 27. token.FileSet ■ ファイル中の位置情報を記録する為の型 ● token.Posは数値として記録 ● 複数ファイル間で共通の値が使われる ● token.FileSet は各ファイルのオフセットを記録 ● オフセットはパース時に記録される ● token.FileSet はparser.Parse*系の関数に 出力引数として渡される ● 人間に分かりやすい情報を取得するにはFileSet.Positionを使用 type Pos int 27 func (s *FileSet) Position(p Pos) (pos Position)
  28. 28. デモ 1: 構文解析と抽象構文木の出力 28 https://youtu.be/lM1Pj6xYxZs Demo Code: https://gist.github.com/tenntenn/833207ce1715089da23936b5dfaebea5
  29. 29. 抽象構文木の探索- ast.Inspect ■ Using ast.Inspect expr, _ := parser.ParseExpr(`v + 1`) ast.Inspect(expr, func(n ast.Node) bool { if n != nil { fmt.Printf("%Tn", n) } return true }) 抽象構文木の探索 *ast.BinaryExpr *ast.Ident *ast.BasicLit Playgroundで実行 ast.Walk はもっと複雑な処理ができる + v 1 BinaryExpr Ident BasicLit 29
  30. 30. 抽象構文木の探索 - 再帰 func traverse(n ast.Node) { switch n := n.(type) { case *ast.Ident: fmt.Println(n.Name) case *ast.BinaryExpr: traverse(n.X) traverse(n.Y) case *ast.UnaryExpr: traverse(n.X) default: fmt.Println(n) } } 識別子:識別子名を出力 2項演算式:各項について再帰的に探索 型で処理を分岐 Playgroundで実行 単項演算式:項を再帰的に探索 30
  31. 31. デモ 2: 識別子の探索 31 https://youtu.be/mpUgaaASvHo Demo Code: https://gist.github.com/tenntenn/299c226c540fccd8386b7148f55f0884
  32. 32. 型チェック /* 型チェックのためのConfigを初期化 */ cfg := &types.Config{Importer: importer.Default()} info := &types.Info{ /* TODO: 結果を保持するためのmapを初期化 */ } pkg, err := cfg.Check("main", fs, []*ast.File{f}, info) if err != nil { /* エラー処理 */ } /* TODO: pkgやinfoを使う処理 */ ■ (*types.Config).Check で型チェックを行う 32
  33. 33. types.Info で型チェックの結果を保持する type Info struct { // Types maps expressions to their types, and for constant // expressions, also their values. Types map[ast.Expr]TypeAndValue // Defs maps identifiers to the objects they define. Defs map[*ast.Ident]Object // Uses maps identifiers to the objects they denote. Uses map[*ast.Ident]Object // Implicits maps nodes to their implicitly declared objects, if any. Implicits map[ast.Node]Object // Selections maps selector expressions (excluding qualified identifiers) // to their corresponding selections. Selections map[*ast.SelectorExpr]*Selection // Scopes maps ast.Nodes to the scopes they define. Scopes map[ast.Node]*Scope // InitOrder is the list of package-level initializers in the order in which // they must be executed. InitOrder []*Initializer } 33
  34. 34. デモ 3: Gopher型の探索 34 https://youtu.be/AuSDtmiMaXI Demo Code: https://gist.github.com/tenntenn/134d6965f75efe5eb6f6edf7b0ebe509
  35. 35. Goにおける静的解析 〜応用編〜 35
  36. 36. 実際のツール開発で必要なことの例 36 ■ パッケージ名からソースコードの一覧を取得する ● 1ファイルを対象とすることはあまりない ● その環境でビルド対象のファイルを対象にしたい ○ ビルドタグなどでビルド対象を決めている場合 ■ 抽象構文木の複雑なトラバース ● 親ノードの情報を元に処理する ● 抽象構文木に変更をいれる
  37. 37. パッケージ名からファイル一覧を取得 ■ パッケージ名からソースディレクトリを取得 ● go listを用いてパッケージ名からディレクトリ一覧を取得 ■ ビルドに必要な情報を集める ● go/buildパッケージを用いる ● ImportDirで取得した*build.Packageにファイル一覧情報がある 37 $ go list -f "{{.Dir}}" | grep -v vendor pkg, err := build.ImportDir(dir, build.IgnoreVendor)
  38. 38. パースする際に不要なファイルを除外 ■ ビルド対象のものだけをパースする ● *build.PacakgeのGoFilesに含まれているものだけ ● parser.ParseDirの第3引数はフィルター関数 ○ trueを返すものだけパースする 38 parser.ParseDir(fset, dir, func(fi os.FileInfo) bool { for _, df := range pkg.GoFiles { if gf == fi.Name() { return true } } return false }, 0)
  39. 39. 抽象構文木の変更 39 ■ 抽象構文木を探索&条件マッチするノードの置き換え ● 2項演算式中の第2項だけを変更する ● 1ノードだけみるast.Inspectやast.Walkでは難しい! ○ go/ast: provide AST Rewrite functionality (ast.Walk is not good enough) + X Y BinaryExpr Ident + X 2 BasicLit
  40. 40. astutil.Apply関数 ■ 抽象構文木を変更するための関数 ● golang.org/x/tools/go/ast/astutilパッケージで提供 ● preとpostというノードに適用する関数を渡す ○ pre: 子ノードを処理する前に呼ばれる関数 ○ post: 子ノードを処理した後に呼ばれる関数 ● 変更した結果をresultとして返す ○ ルートが変更された場合はrootとresultが変わる 40 参考:astutil.Applyで抽象構文木を置き換える func Apply(root ast.Node, pre, post ApplyFunc) (result ast.Node)
  41. 41. ApplyFunc型とCursor型 ■ ApplyFunc型 ● astutil.Apply関数に各ノードに再帰的に適用される関数 ■ Cursor型 ● 現在注目しているノードに関する情報を保持する型 ● ノードの情報を取得するメソッド ○ Node、Index、Parent、Name ● ノードの操作を行うメソッド ○ Replace、Delete、InsertBefore、InsertAfter 41 type ApplyFunc func(*Cursor) bool
  42. 42. ノードの情報を取得するメソッド ■ Node() ast.Node ● 現在注目のノード取得する ■ Parent() ast.Node ● 注目ノードの親ノードを取得 ■ Name() string ● 親ノードにどういう名前でノードを管理されているか ○ 例:BinaryExprの場合、2つの項はXとY ■ Index() int ● 親ノードでスライスとして管理していた場合にそのインデックス ○ 例:関数の引数など ● Nameメソッドだけで区別できない場合に使う 42
  43. 43. ノードを変更するメソッド ■ Replace(n ast.Node) ● 現在注目のノードを指定したノードで入れ替える ■ InsertBefore(n ast.Node) ● スライスで管理されているノードが対象 ● 現在注目のノードの前にノードを追加する ■ InsertAfter(n ast.Node) ● 現在注目のノードの後ろにノードを追加する ■ Delete() ● 現在注目のノードを削除する 43
  44. 44. ノードの更新の例 44 expr, err := parser.ParseExpr(`x+y`) if err != nil { log.Fatal(err) } n := astutil.Apply(expr, func(cr *astutil.Cursor) bool { if _, ok := cr.Parent().(*ast.BinaryExpr); !ok { return true } if cr.Name() == "Y" { cr.Replace(&ast.BasicLit{Kind:token.INT, Value:"2"}) } return true }, nil) if err := format.Node(os.Stdout, token.NewFileSet(), n); err != nil { log.Fatalln("Error:", err) } fmt.Println()
  45. 45. ノードを操作する際の注意点 ■ ノードの操作は破壊的に行われる ● コピーが作られるわけではない ● Apply関数の戻り値はちゃんと受け取る ■ ノードの操作は即時反映される ● preとpostは実行された瞬間にそのノードに適用される 45
  46. 46. ノードの削除の例(失敗例) 46 ■ 第1引数だけを削除したい expr, err := parser.ParseExpr(`func(x, y int){}(10, 20)`) if err != nil { log.Fatal(err) } n := astutil.Apply(expr, func(cr *astutil.Cursor) bool { if cr.Name() == "Args" && cr.Index() == 0 { cr.Delete() } return true }, nil) if err := format.Node(os.Stdout, token.NewFileSet(), n); err != nil { log.Fatalln("Error:", err) } fmt.Println()
  47. 47. ノードの削除の例(修正版) 47 ■ 第1引数を一度だけ削除する expr, err := parser.ParseExpr(`func(x, y int){}(10, 20)`) if err != nil { log.Fatal(err) } var once sync.Once n := astutil.Apply(expr, func(cr *astutil.Cursor) bool { if cr.Name() == "Args" && cr.Index() == 0 { once.Do(cr.Delete) } return true }, nil) if err := format.Node(os.Stdout, token.NewFileSet(), n); err != nil { log.Fatalln("Error:", err) } fmt.Println()
  48. 48. まとめ ■ 静的解析は開発ツールと相性がいい ● 標準のツールでも使われている ● 自作することが可能 ■ Goで静的解析を行うのは簡単 ● 静的型付け、シンプルな文法、goパッケージ ■ goパッケージで簡単に静的解析を行える ● 字句解析、構文解析、型チェック 48
  49. 49. Thank you! twitter: @tenntenn Qiita: tenntenn connpass: tenntenn 49

×