https://tenntenn.dev/ja/posts/2021-04-11-gqlanalysis/

GraphQLの静的解析ライブラリgqlanalysis

副業をしているAppify TechnologiesにてGraphQLの静的解析ツールをGoで書けるライブラリgqlanalysisを作りました。またそれに合わせクエリのセレクションにidの追加忘れを指摘するlackidgqlgoオーガナイゼーションで開発した静的解析ツールをまとめて実行できる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