GraphQLの静的解析基盤を作った

April 11, 2021

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

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社に感謝致します。特にレビューしてくれたそな太さんありがとうございます!!