【Go】ast(dst)と型情報からコードを生成する(partial-json patcher etc)
お題
で作った、github.com/ngicks/und
以下で定義される型は、
struct tagでvalidationルールをつけることで、このフィールドはないといけないというのを表現している
und:"required"
だったらフィールドがsome/definedでないといけないという感じ。
ではこのタグ情報と元のフィールドの構造を用いて、requiredならoption.Option[T]
をT
で置き換えたようなstructを生成できたら便利だよなと思ってる。
astの解析
解析はいろんな方法があるけどとりあえずgolang.org/x/tools/go/packages
を使っていろんなの方法の大体すべてをやってくれる。
cfg := &packages.Config{
Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedImports |
packages.NeedDeps | packages.NeedExportFile | packages.NeedTypes |
packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedModule |
packages.NeedName, // almost all load bits. We'll reduce option as many as possible.
}
pkgs, err := packages.Load(cfg, *inputPkg)
if err != nil {
panic(err)
}
-
Types
fieldは*types.Packages
であるので、ScopeやLookupをとれる。 -
Syntax
fieldは[]*ast.File
であるので、各種Decl
やそれに付随するCommentGroup
もっと一緒くたに解析できたら便利なんだけどなあ。多分色々な都合により分けられている。
とりあえず、今回はSytax
fieldの*ast.File
のDeclフィールドを探索し、条件に一致していたらastutil.Apply
でastの置換を行って、それをgo/printer
でプリントしてファイルとして書きだす。
*ast.CommentGroup
のText()
はディレクティブコメントを削除する。
List
を直接走査してディレクティブコメントを探す必要がある
for _, c := range x.Doc.List {
text, ok := strings.CutPrefix(c.Text, "//")
if !ok {
text, _ = strings.CutPrefix(text, "/*")
}
if strings.HasPrefix(text, "undgen:ignore") {
fmt.Printf("ignored\n")
}
}
以下のAll
型から、AllPlain
型が生成できるところまで実装
type All struct {
Foo string
Bar *string
Baz *struct{}
Qux []string
OptRequired option.Option[string] `und:"required"`
OptNullish option.Option[string] `und:"nullish"`
OptDef option.Option[string] `und:"def"`
OptNull option.Option[string] `und:"null"`
OptUnd option.Option[string] `und:"und"`
OptDefOrUnd option.Option[string] `und:"def,und"`
OptDefOrNull option.Option[string] `und:"def,null"`
OptNullOrUnd option.Option[string] `und:"null,und"`
OptDefOrNullOrUnd option.Option[string] `und:"def,null,und"`
UndRequired und.Und[string] `und:"required"`
UndNullish und.Und[string] `und:"nullish"`
UndDef und.Und[string] `und:"def"`
UndNull und.Und[string] `und:"null"`
UndUnd und.Und[string] `und:"und"`
UndDefOrUnd und.Und[string] `und:"def,und"`
UndDefOrNull und.Und[string] `und:"def,null"`
UndNullOrUnd und.Und[string] `und:"null,und"`
UndDefOrNullOrUnd und.Und[string] `und:"def,null,und"`
ElaRequired elastic.Elastic[string] `und:"required"`
ElaNullish elastic.Elastic[string] `und:"nullish"`
ElaDef elastic.Elastic[string] `und:"def"`
ElaNull elastic.Elastic[string] `und:"null"`
ElaUnd elastic.Elastic[string] `und:"und"`
ElaDefOrUnd elastic.Elastic[string] `und:"def,und"`
ElaDefOrNull elastic.Elastic[string] `und:"def,null"`
ElaNullOrUnd elastic.Elastic[string] `und:"null,und"`
ElaDefOrNullOrUnd elastic.Elastic[string] `und:"def,null,und"`
ElaEqEq elastic.Elastic[string] `und:"len==1"`
ElaGr elastic.Elastic[string] `und:"len>1"`
ElaGrEq elastic.Elastic[string] `und:"len>=1"`
ElaLe elastic.Elastic[string] `und:"len<1"`
ElaLeEq elastic.Elastic[string] `und:"len<=1"`
ElaEqEquRequired elastic.Elastic[string] `und:"required,len==2"`
ElaEqEquNullish elastic.Elastic[string] `und:"nullish,len==2"`
ElaEqEquDef elastic.Elastic[string] `und:"def,len==2"`
ElaEqEquNull elastic.Elastic[string] `und:"null,len==2"`
ElaEqEquUnd elastic.Elastic[string] `und:"und,len==2"`
ElaEqEqNonNull elastic.Elastic[string] `und:"values:nonnull,len==3"`
}
type AllPlain struct {
Foo string
Bar *string
Baz *struct{}
Qux []string
OptRequired string `und:"required"`
OptNullish *struct{} `und:"nullish"`
OptDef string `und:"def"`
OptNull *struct{} `und:"null"`
OptUnd *struct{} `und:"und"`
OptDefOrUnd option.Option[string] `und:"def,und"`
OptDefOrNull option.Option[string] `und:"def,null"`
OptNullOrUnd *struct{} `und:"null,und"`
OptDefOrNullOrUnd option.Option[string] `und:"def,null,und"`
UndRequired string `und:"required"`
UndNullish option.Option[*struct{}] `und:"nullish"`
UndDef string `und:"def"`
UndNull *struct{} `und:"null"`
UndUnd *struct{} `und:"und"`
UndDefOrUnd option.Option[string] `und:"def,und"`
UndDefOrNull option.Option[string] `und:"def,null"`
UndNullOrUnd option.Option[*struct{}] `und:"null,und"`
UndDefOrNullOrUnd und.Und[string] `und:"def,null,und"`
ElaRequired []option.Option[string] `und:"required"`
ElaNullish option.Option[*struct{}] `und:"nullish"`
ElaDef []option.Option[string] `und:"def"`
ElaNull *struct{} `und:"null"`
ElaUnd *struct{} `und:"und"`
ElaDefOrUnd option.Option[[]option.Option[string]] `und:"def,und"`
ElaDefOrNull option.Option[[]option.Option[string]] `und:"def,null"`
ElaNullOrUnd option.Option[*struct{}] `und:"null,und"`
ElaDefOrNullOrUnd elastic.Elastic[string] `und:"def,null,und"`
ElaEqEq [1]option.Option[string] `und:"len==1"`
ElaGr []option.Option[string] `und:"len>1"`
ElaGrEq []option.Option[string] `und:"len>=1"`
ElaLe []option.Option[string] `und:"len<1"`
ElaLeEq []option.Option[string] `und:"len<=1"`
ElaEqEquRequired [2]option.Option[string] `und:"required,len==2"`
ElaEqEquNullish option.Option[[2]option.Option[string]] `und:"nullish,len==2"`
ElaEqEquDef [2]option.Option[string] `und:"def,len==2"`
ElaEqEquNull option.Option[[2]option.Option[string]] `und:"null,len==2"`
ElaEqEquUnd option.Option[[2]option.Option[string]] `und:"und,len==2"`
ElaEqEqNonNull [3]string `und:"values:nonnull,len==3"`
}
生成するコード
今回の目標は、
- Patcher
- 対象となるstruct typeの、あらゆるフィールドをsliceund.Undでラップし、
json:",omitempty"
を付け足すことでpartial jsonによるpatchを実現する
- 対象となるstruct typeの、あらゆるフィールドをsliceund.Undでラップし、
- Valdator
-
und:""
struct tagの内容からsomeじゃないいけないとかそういうのをvalidateする
-
- Plain
-
und:""
struct tagの内容からsomeでないといけないならoption.Option[T]
をTにアンラップしたような型を作成する
-
見つけたいもの
上記のすべてを叶えるためには
- 受け取ったパッケージ内のすべての型宣言を列挙
- 特定の型(i.e.
option.Option[T]
)を含むフィールドの検知 - 特定のメソッドを実装する型をフィールドに含む型の検知
- さらに上記の2つの検知にかかった型をフィールドに含む方を含む方を芋づる式に検知
する必要がある
検知方法の検討
型の依存性をグラフ化して依存関係を探索、条件を満たす型から上に向けてトラバースすることで生成対象の型を列挙する。
-
pkgs []*packages.Package
を引数にとる -
pkgs
を全部探索して型を列挙する -
matcher
を引数に取り、これにマッチする型をリストしておく - 型同士の依存関係をエッジとして型依存グラフを形成する
- 依存関係探索時、
pkgs
外の*types.Named
についても関してもmatcherを実行する。マッチする場合、externalとしてリストしておく - マッチした型からupward traverseすることで、芋づる式の検知を可能にする
エッジにはstruct fieldである、mapのvalueである、chan/array/sliceのEltであるなど複数種類があり、更にこれらは複数ネストすることこある(e.g. map[string][][2][]T
)
type paramの取り扱いが難しい
例えば以下のような型がある時
type Foo struct {
Foo Bar[Baz]
}
type Bar[T any] struct {
Bar T
}
type Baz struct {
Baz TargetType
}
Fooはターゲットタイプを含む方を含む型になる
- Type argの追跡は比較的簡単
- ネストした時どうする?
- Raw ↔ Plain変換のために関数を定義しだすとどこに書き出すかとか、1度しか書き出さないようにするとかが大変なので、基本はFooのUndRaw/UndPlainの中で全フィールドを列挙することになる…大変だ!
改題
流れ
- お題
- und関連
- patch, validator, plain
- (実現したいこと)生成したい型の最終イメージ
- exact matchとtransitive match
- und typeをフィールドに含むstructもしくはmap/array/sliceかつ要素がimplementor or implementor wrapped in und typeなどが生成の対象になる
- さらに、生成の対象を含む方も生成の対象になる
- 前者をexact matched、後者をtransitive matchedと呼ぶ
- transitive mactedは言葉の通り、型依存をtransitして見つける(用語の見直しの可能性ある。私はネイティブ日本人であり英語で用語を組み上げるのは難易度が高い)
- 単純な実装方法だと、exactをまず探し出してからからmap[K]Vに記録しといて、2度めの探索時は記録された型に依存する方を探せばよいが
- 依存関係が深いと2度の探索では順序によってはたどり着かない型もできる
- 理屈上依存グラフを形成してexactから自身に依存している型に向けてtraverseすると漏れがない
- chan E等の場合被型変換をかけてもしかたないため、これらを無視する仕組みも必要
iteratorを使用した感想についても述べる
新たに定義する型はらっぷ、アンラップの関係であるのでほとんど定義を使い回せる。そのためastの書き換えは都合がいい
メソッドの書き出しは関数分離する都合上、text/template
は使いにくかった、import declも上記のast書き換えな影響で使い回され、各部での協調が必要とされる。そのため外部からimport declをコントロールできないgithub.com/dave/jennifer
は採用できなかった
タイプグラフの概略
- 全てのtype specを列挙
- 列挙された型はpackage pathとlocal nameによって同定(ident)され、nodeとなる
- named type → named typeへの経路を探索する
- それらをedgeとして記録する
- edgeは経路stackを持つ
namedからnamedへの探索であるので、この時点では型の循環による無限ループは心配しなくて良い(traveres時には気をつける必要がある)
type Example struct {
Foo Foo
Bar map[string]Bar
Baz [3]Baz
}
type Foo string
type Bar int
type Baz bool
この場合、Example
は
- struct→Foo
- struct,map→Bar
- struct,array→Baz
という経路スタックを持つedgeで各childtypeと繋がる
ここで、重要なのはFoo,Bar,Bazと言った各childrenからもparentに向けてedgeを繋いでおく、無向循環グラフであるということだ
双方向に参照できることで、exact matchからそれを依存する型に向けてtraverseすることができるし、型生成時にはchildrenとの関係性がわかるので、型性に必要な情報として振る舞える
- named typeの生成