Top Banner
GoASTをいじくって 新しいツールを作る わかめ まさひろ
43

GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Aug 06, 2015

Download

Technology

Masahiro Wakame
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

GoのASTをいじくって 新しいツールを作る

わかめ まさひろ

Page 2: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

わかめ まさひろ @v vakame

TypeScript

Masahiro Wakame

DefinitelyTyped

appengine

photo from golang.org/doc/gopher/

Page 3: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

めんどいことはしたくない

誰だってそうする

俺だってそうする

Page 4: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

encoding/json

play.golang.org/p/T9uO25D2xz

… type Game struct { ID int64 `json:"id" ̀ Title string `json:"title" ̀ Price int `json:"price" ̀ InDevelopment bool `json:"inDevelopment" ̀ ShippedAt time.Time `json:"shippedAt" ̀} func main() { game := &Game{ ID: 1, Title: "Splatoon", Price: 5700, InDevelopment: false, } b, _ := json.Marshal(game) fmt.Println(string(b))}

Page 5: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

encoding/json… type Game struct { ID int64 `json:"id" ̀ Title string `json:"title" ̀ Price int `json:"price" ̀ InDevelopment bool `json:"inDevelopment" ̀ ShippedAt time.Time `json:"shippedAt" ̀} func main() { game := &Game{ ID: 1, Title: "Splatoon", Price: 5700, InDevelopment: false, } b, _ := json.Marshal(game) fmt.Println(string(b))}

手書き!? 正気か!?!?

めんどい “ 閉じるの忘れる typoる

Page 6: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

jwg 作った//go:generate jwg -output model_json.go .package sample… // +jwgtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time} func main() { game := &Game{ ID: 1, Title: "Splatoon", Price: 5700, InDevelopment: false, } jsonObj, _ := NewGameJsonBuilder().AddAll().Convert(game) b, _ := json.Marshal(jsonObj) fmt.Println(string(b))}

jwg = Json Wrapper Generator

Page 7: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

//go:generate jwg -output model_json.go .package sample… // +jwgtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time} func main() { game := &Game{ ID: 1, Title: "Splatoon", Price: 5700, InDevelopment: false, } jsonObj, _ := NewGameJsonBuilder().AddAll().Convert(game) b, _ := json.Marshal(jsonObj) fmt.Println(string(b))}

go generate 使う!

コメントにタグ書く(標準仕様などない!

生成したコード利用だ!

jwg 作った

Page 8: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

自動生成!

type GameJson struct { ID int64 `json:"id,omitempty" ̀ Title string `json:"title,omitempty" ̀ Price int `json:"price,omitempty" ̀ InDevelopment bool `json:"inDevelopment,omitempty" ̀ ShippedAt time.Time `json:"shippedAt,omitempty" ̀}

Page 9: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

その他!type GameJson func (orig *GameJson) Convert() (*Game, error) type GameJsonBuilder func NewGameJsonBuilder() *GameJsonBuilder func (b *GameJsonBuilder) Add(info *GamePropertyInfo) *GameJsonBuilder func (b *GameJsonBuilder) AddAll() *GameJsonBuilder func (b *GameJsonBuilder) Convert(orig *Game) (*GameJson, error) func (b *GameJsonBuilder) ConvertList(orig []*Game) (GameJsonList, error) func (b *GameJsonBuilder) Marshal(orig *Game) ([]byte, error) func (b *GameJsonBuilder) Remove(info *GamePropertyInfo) *GameJsonBuilder type GameJsonList func (jsonList GameJsonList) Convert() ([]*Game, error) type GamePropertyDecoder type GamePropertyEncoder type GamePropertyInfo *JsonBuilder

*Property(De|En)coder *PropertyInfo

Page 10: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Web API作成用

play.golang.org/p/5wYA62Njvn

func (b *GameJsonBuilder) AddSite() *GameJsonBuilder { b.AddAll() b.Remove(b.ID) // IDは内部情報なのでいらない b.Price.Encoder = func(src *Game, dest *GameJson) error { if !src.InDevelopment { dest.Price = src.Price // 開発中じゃない時だけ価格を出すよ! } return nil } return b } func main() { game := &Game{ ID: 2, Title: "Secret of Yaba", Price: 9999, InDevelopment: true, } jsonObj, _ := NewGameJsonBuilder().AddSite().Convert(game) b, _ := json.Marshal(jsonObj) fmt.Println(string(b))}

{ "title":"Secret of Yaba”, “inDevelopment":true, “shippedAt":"0001-01-01T00:00:00Z" }

実行結果→

Page 11: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

この間公開しました!

生成コードは特に依存なし

Page 12: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

みんなも作ろう!

Page 13: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

主張• コード生成 is 便利

• GoだとGenericsないしコード増えがち

• コンパイル時チェックの恩恵!

• 文字列で指定とか時代遅れだよね~

• 元コード→データ化→加工→生成!

• まずはソースコードを解析しないと!

Page 14: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

正規表現で頑張る

http://play.golang.org/p/fsOl7CcjgB

package mainimport ( "fmt" "regexp") func main() { code := ` type Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time } ` re:=regexp.MustCompile(`\s*type\s+([a-zA-Z]+)\s+struct\s+\{\n(?:\s*([a-zA-Z0-9]+)\s+([a-zA-Z0-9\.]+)\s*\n)*\s*}`) result := re.FindAllStringSubmatch(code,-1) fmt.Printf("%#v", result)}

Page 15: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

copyright @shati_ko

Page 16: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

ASTを活用する

• AST = Abstract Syntax Tree

• 本来はコンパイラ内部の中間表現

• ソースコードをデータとして使える!

• コード解析はライブラリに任せよう!

• 解析後のコード組立に専念できる!

Page 17: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

copyright @shati_ko

Page 18: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

AST? コード生成??

Page 19: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Game struct → ASTtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time}

ast = go/ast package

ast.GenDecl

Page 20: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Game struct → ASTtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time}

ast = go/ast package

ast.GenDecl ast.TypeSpec

type ( A struct { Foo string } B struct { Bar string } )

こういう記法もある(怖い

Page 21: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Game struct → ASTtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time}

ast = go/ast package

ast.GenDecl ast.TypeSpec ast.Ident

Page 22: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Game struct → ASTtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time}

ast = go/ast package

ast.GenDecl ast.TypeSpec ast.Ident ast.StructType

Page 23: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Game struct → ASTtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time}

ast = go/ast package

ast.GenDecl ast.TypeSpec ast.Ident ast.StructType ast.FieldList

Page 24: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Game struct → ASTtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time}

ast = go/ast package

ast.GenDecl ast.TypeSpec ast.Ident ast.StructType ast.FieldList ast.Field type A struct {

Foo, Bar string }

こういう記法もある(怖い

Page 25: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Game struct → ASTtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time}

ast = go/ast package

ast.GenDecl ast.TypeSpec ast.Ident ast.StructType ast.FieldList ast.Field ast.Ident

Page 26: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Game struct → ASTtype Game struct { ID int64 Title string Price int InDevelopment bool ShippedAt time.Time}

ast = go/ast package

ast.GenDecl ast.TypeSpec ast.Ident ast.StructType ast.FieldList ast.Field ast.Ident ast.Ident

Page 27: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

AST→コード生成

_人人人人人人人人人人人人_ > 文字列組み立て頑張る < ‾Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y‾

まじかまじだ

Page 28: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

開発のコツと構造ここでは、struct読み取り→

ラッパ生成の流れに絞って解説する

Page 29: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

ツール開発の流れ1. 処理対象(のstruct)を決める2. コード生成結果を手書きする

•名前を機械的に考えてつけよう3. 必要な俺形式のデータ構造を設計する

•ASTから取れるか?不足はないか?•型情報取れなくて辛いパターンある

4. 頑張ってAST取って変換して生成処理書く

Page 30: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Goコードの構造// generated by jwg -output model_json.go .; DO NOT EDITpackage sampleimport ( "encoding/json" "time" ) // for Gametype GameJson struct { ID int64 `json:"id,omitempty" ̀ Title string `json:"title,omitempty" ̀ Price int `json:"price,omitempty" ̀ InDevelopment bool `json:"inDevelopment,omitempty" ̀ ShippedAt time.Time `json:"shippedAt,omitempty" ̀}

PackageClauseImportDecl

TopLevelDecl

Page 31: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

俺形式が必要な理由// generated by jwg -output model_json.go .; DO NOT EDITpackage sampleimport ( "encoding/json" "time" ) // for Gametype GameJson struct { ID int64 `json:"id,omitempty" ̀ Title string `json:"title,omitempty" ̀ Price int `json:"price,omitempty" ̀ InDevelopment bool `json:"inDevelopment,omitempty" ̀ ShippedAt time.Time `json:"shippedAt,omitempty" ̀}

正しいPackageClauseの生成には、

TopLevelDecl生成結果の把握が必要! etc..

Page 32: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

代表的な俺形式

• Source (生成結果ソース全体)

• Struct (生成するstruct1個分)

• Field (↑のfield1個分)

• Tag (↑に付属するtag情報)

Page 33: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

jwgの場合// BuildStruct represents source code of assembling..type BuildSource struct { g *genbase.Generator pkg *genbase.PackageInfo typeInfos genbase.TypeInfos Structs []*BuildStruct} // BuildStruct represents struct of assembling..type BuildStruct struct { parent *BuildSource typeInfo *genbase.TypeInfo Fields []*BuildField} // BuildField represents field of BuildStruct.type BuildField struct { parent *BuildStruct fieldInfo *genbase.FieldInfo Name string Embed bool Tag *BuildTag} // BuildTag represents tag of BuildField.type BuildTag struct { field *BuildField Name string Ignore bool // e.g. Secret string `json:"-" ̀ DoNotEmit bool // e.g. Field int `json:",omitempty" ̀ String bool // e.g. Int64String int64 `json:",string" ̀}

Page 34: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

genbaseのご紹介• 3つほどコード生成ツール作った

• 定形処理の存在に気がつく• AST読み込み

• 指定されたorタグ付きstructの収集

• import句の管理• コード組み立て・フォーマット• その他便利関数とかgithub.com/favclip/genbase

参考:typewriter

Page 35: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

そして気合func (st *BuildStruct) emit(g *genbase.Generator) error { g.Printf("// for %s\n", st.Name()) // generate FooJson struct from Foo struct g.Printf("type %sJson struct {\n", st.Name()) for _, field := range st.Fields { if field.Tag.Ignore { continue } postfix := "" if field.WithJWG() { postfix = "Json" } tagString := field.Tag.TagString() if tagString != "" { tagString = fmt.Sprintf("`%s`", tagString) } if field.Embed { g.Printf("%s%s %s\n", field.fieldInfo.TypeName(), postfix, tagString) } else { g.Printf("%s %s%s %s\n", field.Name, field.fieldInfo.TypeName(), postfix, tagString) } } g.Printf("}\n\n") g.Printf("type %[1]sJsonList []*%[1]sJson\n\n", st.Name()) // generate property builder g.Printf("type %[1]sPropertyEncoder func(src *%[1]s, dest *%[1]sJson) error\n\n", st.Name()) g.Printf("type %[1]sPropertyDecoder func(src *%[1]sJson, dest *%[1]s) error\n\n", st.Name()) // generate property info g.Printf(` type %[1]sPropertyInfo struct { name string Encoder %[1]sPropertyEncoder Decoder %[1]sPropertyDecoder } `, st.Name()) // generate json builder g.Printf("type %sJsonBuilder struct {\n", st.Name())

↓ざっくり500行続く

Page 36: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

デカさ• genbase

• ざっくり580行くらい

• jwg

• ざっくり850行くらい

• 生成後コード読んでから読めば理解る

• …んじゃないかな多分

Page 37: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Tips

• 埋め込みstructは敵

• 生成すべきコードがどんどん複雑に…

• fieldの型がstructだと絶望

• ASTだけでは型の詳細な情報がない• 生成コードないとコンパイル通らん

• Printfの %[1]s 記法マジ便利

Page 38: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

I ♥ Pull Request

• よりGoらしい書き方できるよ!

• より効率の良い実装があるよ!

• Template使えよ!

• text/template は気に入らなかった…

• なんかないですかね?github.com/favclip

Page 39: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

宣伝

Page 40: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

We are hiring!

•開発:テレビ朝日

• jwg, genbase 他 爆誕!

• http://www.favclip.com/

• appengine/go 開発者絶賛募集中!

Page 41: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

Goに対する感想

Page 42: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

疑問• ライブラリのリビジョン?

• jwg 非互換な変更していいのかしら

• embedしたstructのメソッド呼び奴

• 外側のstructがreceiverになってほし

• Generics欲しい気持ちが抑えられない

Page 43: GoCon 2015 Summer GoのASTをいじくって新しいツールを作る

怒り💢• stringのslice取ると[]byteなのやめて💢

• 1文字=1バイトマンが作るライブラリ

• 再帰的なパッケージ参照許して💢

• 1パッケージが際限なくでかくなる…

• err != nil 毎回やるのだるい💢