Closed14

【Go】ast(dst)と型情報からコードを生成する(partial-json patcher etc)

ngicksngicks

お題

https://zenn.dev/ngicks/articles/go-json-undefined-or-null-slice

で作った、github.com/ngicks/und以下で定義される型は、
struct tagでvalidationルールをつけることで、このフィールドはないといけないというのを表現している

https://github.com/ngicks/und/blob/main/validate/validate.go

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でプリントしてファイルとして書きだす。

ngicksngicks

*ast.CommentGroupText()はディレクティブコメントを削除する。

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

以下の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"`
}
ngicksngicks

生成するコード

今回の目標は、

  • Patcher
    • 対象となるstruct typeの、あらゆるフィールドをsliceund.Undでラップし、json:",omitempty"を付け足すことでpartial jsonによるpatchを実現する
  • 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することで、芋づる式の検知を可能にする
ngicksngicks

エッジにはstruct fieldである、mapのvalueである、chan/array/sliceのEltであるなど複数種類があり、更にこれらは複数ネストすることこある(e.g. map[string][][2][]T)

ngicksngicks

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の中で全フィールドを列挙することになる…大変だ!
ngicksngicks

流れ

  • お題
    • 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等の場合被型変換をかけてもしかたないため、これらを無視する仕組みも必要
ngicksngicks

iteratorを使用した感想についても述べる

ngicksngicks

新たに定義する型はらっぷ、アンラップの関係であるのでほとんど定義を使い回せる。そのためastの書き換えは都合がいい

メソッドの書き出しは関数分離する都合上、text/templateは使いにくかった、import declも上記のast書き換えな影響で使い回され、各部での協調が必要とされる。そのため外部からimport declをコントロールできないgithub.com/dave/jenniferは採用できなかった

ngicksngicks

タイプグラフの概略

  • 全ての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との関係性がわかるので、型性に必要な情報として振る舞える

このスクラップは2日前にクローズされました