GraphQLの静的解析基盤を作った
April 11, 2021
GraphQLの静的解析ライブラリgqlanalysis
副業をしているAppify TechnologiesにてGraphQLの静的解析ツールをGoで書けるライブラリgqlanalysisを作りました。またそれに合わせクエリのセレクションにid
の追加忘れを指摘するlackidとgqlgoオーガナイゼーションで開発した静的解析ツールをまとめて実行できるgqlintも公開されています。
gqlanalysisを用いるとGraphQLのスキーマやクエリファイルに対するLinterを簡単に作ることができます。gqlanalysisはGoの静的解析ツールライブラリのgo/analysisに似た構造で作ってあります。
go/analysisと同様にAnalyzerという単位で解析を行います。Analyzerは別のAnalyzerの解析結果を用いることができるため、静的解析ツールをモジュール化できます。各Analyzerはゴルーチンで1度だけ実行されます。
.graphql
ファイル(.gql
や.ql
などの拡張子の場合もある)のパースはgqlanalysisがgqlparserを用いて自動で行います。そのため、Analyzerの開発者は、作りたい静的解析ツールのロジックのみに集中して開発できます。
Linterの解析部Analyzerの作り方
Analyzerはgqlanalysis.Analyzer
型という構造体として定義されています。Analyzer構造体のフィールドを埋めることにより静的解析ツールを作成できます。
例えば、名前が”Gopher”で始まるクエリを検出するAnalyzerは以下のように書けます。
packge findgopher
import (
"strings"
"github.com/gqlgo/gqlanalysis"
)
var Analyzer = &gqlanalysis.Analyzer{
Name: "findgopher",
Doc: `find a query which name begin "Gopher"`,
Run: run,
}
func run(pass *gqlanalysis.Pass) (interface{}, error) {
for _, q := pass.Queries {
for _, op := range Operations {
if op.Operation == ast.Query &&
strings.HasPrefix(op.Name, "Gopher") {
pass.Reportf(op.Position, "NG")
}
}
}
return nil, nil
}
Pass
型には依存するAnalyzerの解析結果やクエリやスキーマのパース結果が格納されています。(*gqlanalysis.Pass).Reportf
メソッドを用いることで該当箇所を指定してレポートできます。
より実践的な実装例は筆者が開発したlackidを参考にすると良いでしょう。
テストの書き方
go/analysisと同様にテストも簡単に作成できます。以下のようにテストデータを生成し、コメントでレポートされるであろう箇所にメッセージをwant
コメントともに正規表現で記述するだけです。
query Gopher { # want "NG"
...
}
テストコードは以下のようにanalysistest
パッケージを用いるだけです。
package findgopher_test
import (
"testing"
"github.com/gqlgo/gqlanalysis/analysistest"
"example.com/findgopher"
)
func Test(t *testing.T) {
testdata := analysistest.TestData(t)
analysistest.Run(t, testdata, findgopher.Analyzer, "a")
}
テストに使用する.graphql
ファイルはtestdata
以下に配置します。analysistest.Run
関数が指定したディレクトリ以下の*.graphql
ファイルを解析対象としてテストを行います。
testdata
└── a
├── query
│ ├── mutation.graphql
│ ├── query.graphql
│ └── subscription.graphql
└── schema
├── model.graphql
└── schema.graphql
Linterのmain関数の作り方
multicheker.Main
関数に*gqlanalysis.Analyzer
型の値を指定すると簡単にLinterのmain
関数を作れます。Analyzerは複数指定することができ、各Analyzerは独立したゴルーチンで実行されます。
package main
import (
"github.com/gqlgo/gqlanalysis/multichecker"
"example.com/findgopher"
)
func main() {
multichecker.Main(
findgopher.Analyzer,
)
}
筆者が開発したLinterのgqlintを参考にすると良いでしょう。
作成したLinterの実行
作成したLinterは以下のように実行できます。Linterは-schema
フラグと-query
フラグで指定したパターンにマッチするファイルを解析対象とします。パターンのマッチングにはmattn/go-zglobを用いています。なお、-schema
フラグのデフォルト値はschema/**/*.graphql
で-query
フラグはquery/**/*.graphql
です。
$ findgopher -schema="server/graphql/schema/**/*.graphql" -query="client/**/*.graphql"
また、スキーマはイントロスペクションにも対応しています。以下のように-schema
フラグにエンドポイントを指定します。
$ findgopher -schema="https://example.com/graphql" -query="client/**/*.graphql"
今後の展望
gqlanalysisにはgo/analysisのエコシステムをさらに導入しようと考えています。
具体的には、
- Factのようなオブジェクトに対する述語を表現する機能
- SuggestedFixライクな仕組みによる自動修正
- go/analysisのAnalyzerと互換性を持たせGoのコードとの関連を基に解析できるようにする
- skeletonライクなスケルトンコード生成ツール
などがあります。
また、gqlparser
のバリデーションの機能を使ったAnalyzerなども作りたいと考えています。
みなさんもぜひGoでGraphQL
のLinterを作りましょう!
謝辞
副業という形にも関わらずコミュニティへの貢献を配慮し開発したコードをOSSにすることを許可して頂いたAppify Technologies社に感謝致します。特にレビューしてくれたそな太さんありがとうございます!!