Slide 1

Slide 1 text

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. GraphQLの静的解析 ツールを作った 2022/01/15(土) tenntenn Conference 2022 資料:https://tenn.in/gqlanalysis 動画:https://tenn.in/gqlanalysis-video

Slide 2

Slide 2 text

上田拓也 Go ビギナーズ
 Go Conference
 @tenntenn tenntenn.dev Google Developer Expert (Go) 一般社団法人 Gophers Japan 代表理事 Experts Team

Slide 3

Slide 3 text

この活動を支える企業 ■ 株式会社Appify Technologies ● https://appify-inc.com/ ● 採用情報:https://herp.careers/v1/appify/MDDmrTNJ2hUo ● https://github.com/gqlgo

Slide 4

Slide 4 text

GraphQLとは ■ APIのためのクエリ言語 ● RESTと違ってクライアントが要求する情報だけ取得できる ● 複数のデータソースから1リクエストで取得できる ■ スキーマベースで開発できる ● GraphSQLのスキーマからGoの構造体定義を生成できる ○ gqlgenを使用する

Slide 5

Slide 5 text

AppifyのGraphQLアーキテクチャ

Slide 6

Slide 6 text

GraphQLの静的解析 ■ スキーマとクエリの静的解析 ● https://github.com/gqlgo/gqlanalysis/ ● Goで書ける ○ GraphQLの静的解析ツールはほとんどがJavaScript ○ 開発言語にGoを使っている場合はGoで書きたい ● パースにgqlparserを使用 ● go/analysisパッケージと似た形で開発できる ○ モジュール化(Analyzer単位での開発) ○ テスト ● gqlskeletonコマンド ○ スケルトンコードを作れる

Slide 7

Slide 7 text

GraphQLの静的解析ツールの例 ■ lackid ● https://github.com/gqlgo/lackid ● スキーマにidフィールドを持つのにクエリに指定されていないもの query GetA() { a { # want "type A has id field but selection a does not have id field" name } } $ lackid -schema="server/graphql/schema/**/*.graphql" -query="client/**/*.graphql" # イントロスペクションにも対応 $ lackid -schema="https://example.com" -query="client/**/*.graphql"

Slide 8

Slide 8 text

gqlanalysis.Analyzer構造体 ■ GraphQLの静的解析ツールの単位 ● Runフィールドに解析の処理をセットする ● 依存するAnalyzerをRequiresフィールドにセットする ● 用途はanalysis.Analyze構造体とほとんど同じ type Analyzer struct { Name string Run func(pass *Pass) (interface{}, error) Doc string Flags flag.FlagSet Requires []*Analyzer ResultType reflect.Type }

Slide 9

Slide 9 text

gqlanalysis.Pass構造体 ■ 静的解析に使う情報が入った構造体 ● Analyzer.Runフィールドセットされた関数の引数で用いる type Pass struct { Analyzer *Analyzer Schema *ast.Schema // スキーマの抽象構文木 Queries []*ast.QueryDocument // クエリの抽象構文木 Comments []*Comment Report func(*Diagnostic) // エラー出力 ResultOf map[*Analyzer]interface{} // 依存するAnalyzerの結果 }

Slide 10

Slide 10 text

gqlanalysis.Diagnostic ■ ast.Position(位置)に関連付けられた静的解析結果 ● 任意の位置へのエラーを表現するために使う ○ 例:〇行目に〇〇というエラーがあります ■ (*gqlanalysis.Pass).Reportfメソッド ● Diagnosticを生成するメソッド ● fmt.Fprintf感覚で使える func (pass *Pass) Reportf( pos ast.Position, // 位置 format string, args ...interface{} )

Slide 11

Slide 11 text

Analyzerの例 var Analyzer = &gqlanalysis.Analyzer{ Name:"simple", Doc:"simple", Run:run } func run(pass *gqlanalysis.Pass) (interface{}, error) { for _, q := range pass.Queries { for _, f := range q.Fragments { for _, sel := range f.SelectionSet { switch sel := sel.(type) { case *ast.Field: if sel.Name == "name" { pass.Reportf(sel.Position, "NG") } } } } } return nil, nil }

Slide 12

Slide 12 text

analysistestパッケージ ■ Analyzerのテストを簡単に行うためのパッケージ ● 実際の解析対象となるソースコードをファイルとして用意すればよい ● テストデータはtestdata以下に用意する ● コメントによって該当行にDiagnosticが出力されるかチェックする func Test(t *testing.T) { testdata := analysistest.TestData(t) analysistest.Run(t, testdata, simple.Analyzer, "a") }

Slide 13

Slide 13 text

テストデータ fragment AFragment1 on A { name # want "NG" } fragment AFragment2 on A { id name # want "NG" ...BFragment } fragment BFragment on A { name # want "NG" } ■ wantで始まるコメントで期待する結果を書く ● 対応するDiagnosticが出力されていればOK 正規表現が使える

Slide 14

Slide 14 text

multichecker ■ multichecker.Main ● 複数のAnalyzerからなるmain関数を提供する ● 各Analyzerはゴールーチンで1回だけ実行される package main import ( "github.com/gqlgo/gqlanalysis/multichecker" "github.com/gqlgo/lackid" "github.com/gqlgo/myanalyzer" ) func main() { multichecker.Main(lackid.Analyzer, myanalyzer.Analyzer) }

Slide 15

Slide 15 text

gqlskeleton ■ gqlanalysis用のスケルトンコードジェネレータ ● 簡単にGraphQLの静的解析ツールを始めることができる ● Analyzer、テストコード、main.goの雛形作ってくれる $ gqlskeleton simple simple ├── cmd │ └── simple │ └── main.go ├── go.mod ├── simple.go ├── simple_test.go └── testdata └── a ├── query │ └── query.graphql └── schema ├── model.graphql ├── mutation.graphql ├── query.graphql ├── schema.graphql └── subscription.graphql $ go install github.com/gqlgo/gqlanalysis/cmd/gqlskeleton@latest

Slide 16

Slide 16 text

gqlskeletonの仕組み ■ skeletonkitを使用 ● https://github.com/gostaticanalysis/skeletonkit ● スケルトンコードジェネレーターを作るためのライブラリ ○ gostaticanalysis/skeletonでも使っている ● テンプレートを用意してあげるだけ楽ちん 参考:io/fsパッケージを用いた テスタブルなコード生成ツールの開発 - Go Conference Online 2021 Autumn

Slide 17

Slide 17 text

デモ

Slide 18

Slide 18 text

今後入る予定の機能 ■ コード生成ライブラリ ● 簡単にGraphQL用コード生成器が作れる(PR#7) var flagOutput string var Generator = &codegen.Generator{ Name: "fragment", Doc: "example of codegen", Run: run } func init() { Generator.Flags.StringVar(&flagOutput, "output", "fragment.kt", "output file") } func run(pass *codegen.Pass) (rerr error) { output, path := pass.CreateTemp("fragment.kt") for _, q := range pass.Queries { if len(q.Fragments) == 0 { continue } tmpl := codegen.NewTemplate(pass, "fragment-template") _, err := tmpl.Funcs(funcMap(pass, tmpl)).Parse(tmplStr) if err != nil { return err } if err := tmpl.ExecuteTemplate(output, "fragments", q.Fragments); err != nil { return err } } if err := exec.Command("ktfmt", path).Run(); err != nil { return err } if _, err := output.Seek(0, io.SeekStart); err != nil { return err } if _, err := io.Copy(pass.Output, output); err != nil { return err } return nil }

Slide 19

Slide 19 text

まとめ ■ GraphQLでも静的解析できる! ● gqlanalysisを使うと簡単に静的解析できる ● gqlskeletonで簡単に始められる ● GoでGraphQLの静的解析ツールが作れる