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

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

Embed Size (px)

Citation preview

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

わかめ まさひろ

わかめ まさひろ @v vakame

TypeScript

Masahiro Wakame

DefinitelyTyped

appengine

photo from golang.org/doc/gopher/

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

誰だってそうする

俺だってそうする

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))}

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る

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

//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 作った

自動生成!

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" ̀}

その他!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

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" }

実行結果→

この間公開しました!

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

みんなも作ろう!

主張• コード生成 is 便利

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

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

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

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

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

正規表現で頑張る

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)}

copyright @shati_ko

ASTを活用する

• AST = Abstract Syntax Tree

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

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

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

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

copyright @shati_ko

AST? コード生成??

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

ast = go/ast package

ast.GenDecl

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 } )

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

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

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

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

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 }

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

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

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

AST→コード生成

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

まじかまじだ

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

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

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

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

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

4. 頑張って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

俺形式が必要な理由// 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..

代表的な俺形式

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

• Struct (生成するstruct1個分)

• Field (↑のfield1個分)

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

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" ̀}

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

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

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

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

参考:typewriter

そして気合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行続く

デカさ• genbase

• ざっくり580行くらい

• jwg

• ざっくり850行くらい

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

• …んじゃないかな多分

Tips

• 埋め込みstructは敵

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

• fieldの型がstructだと絶望

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

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

I ♥ Pull Request

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

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

• Template使えよ!

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

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

宣伝

We are hiring!

•開発:テレビ朝日

• jwg, genbase 他 爆誕!

• http://www.favclip.com/

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

Goに対する感想

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

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

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

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

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

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

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

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

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

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