Goのトップレベルで定義したエラー変数の命名規則をチェックするリンターを作ってみた
トップレベルで宣言されたエラーの名前を正規表現でチェックするtomato3713/go-varErrChecker というカスタムリンターを作成しました。
Go言語ではカスタムエラーを定義するときにトップレベルでErrorインターフェイスを満たす変数を定義し、その変数を返すということを行います。
コードで書くと次のようになります。ErrOccuredSomething
のような変数の名前が命名規則に従っているかをチェックするリンターです。
var ErrOccuredSomething = fmt.Errorf("failed to occured something") func somefunc() error { return ErrOccuredSomething }
作ってから気が付きましたが、似たようなことを行うリンターにgolangci-lintのリンターとして組み込まれているAntonboom/errnameやmgechev/reviveのルールの1つのerror-namingがあります。
tomato3713/go-varErrChecker はデフォルトでは命名規則のチェックに ^Err[\d\w]+$
という正規表現を使うのでErr
を接頭辞に持つ変数ならパスできます。
オプションで正規表現を変えることができるので例えばError
を末尾に持つことをチェックしたいときは
go vet -vettool=varErrChecker -varErrChecker.pattern="^[\d\w]+Error$" .
のように varErrChecker.pattern
オプションに^[\d\w]+Error$
を設定すると実現できます。
独自リンターの実装
Goで静的解析を行うツールを作る場合は、golang.org/x/tools/go/analysis を使うと go vet
に組み込んで実行するツールが簡単に作れます。
また、他のAnalyzerの結果を利用することもできます。
Analysisパッケージを使って実装した場合、静的解析の起点はfunc run(pass *analysis.Pass) (interface{}, error)
です。
varErrCheckerではvarErr.goL35 にあります。
run()
の引数として渡される*analysis.Pass
には、analysis package - golang.org/x/tools/go/analysis - Go Packages にある通り分析対象のパッケージの情報や静的解析結果を報告するための Reportf()
などの関数が含まれています。
varErrCheckerでは[]ast.File
型のフィールド pass.Files
をforループで各ファイルに対して次の手順を繰り返します。
1: トップレベルの変数宣言を見つける: varErr.go#L61
トップレベルの宣言は、ast.File
の Decls
フィールドに格納されています。
https://pkg.go.dev/go/ast#File
Decls []Decl // top-level declarations; or nil
Declの型定義を観ると、Node
と declNode()
を持つインターフェイスとして定義されていて良くわからないですね。そのままだと何もわからないですが、*ast.FuncDecl
や *ast.GenDecl
など定義に関するものが入るので型アサーションを使って型を限定してあげるとそれぞれのフィールドにアクセスできるようになります。今回は変数の宣言なので、*ast.GenDecl
で型アサーションします。
// All declaration nodes implement the Decl interface. type Decl interface { Node declNode() }
型アサーションは、インターフェイス型から実体の型を動的に取り出すために使うx.(T)
という表記のことです。
Import、定数、変数、型宣言も全て*ast.GenDecl
になるので、varErrCheckerでは Specs
フィールドをみてImportや型宣言を除外しています。
const と varが両方とも ast.ValueSpec でまとまっていることが面白いですね。
Tok
フィールドを参照して token.Var
と一致するかで判定したほうがいいかもしれません。
ast.ValueSpec型のフィールドにいるNamesフィールドが宣言された識別子を表しています。 配列になっているのは、次のように一度に複数の識別子が宣言されることがあるためだと思います。
var ( a int b int )
2: 宣言された変数がエラーインターフェイスを持っているかを判定する: varErr.go#L71
インターフェイスを満たしているかの判定はfunc Implements(V Type, T *Interface) boolを利用できます。
T
インターフェイスを渡しますが、今回判定したいエラーインターフェイスの *type.Interface
型オブジェクトは次のようにすると得られます。
var errorType = types.Universe.Lookup("error").Type() var errorInterface = errorType.Underlying().(*types.Interface)
判定対象となるV
はオブジェクトの型を渡します。*ast.Ident型のオブジェクトであれば、次のようにするとオブジェクトのタイプを得られます。なので、[]*ast.Ident
型のast.ValueSpec.Names
の各要素に対して同じことをして型情報を取得しています。
obj := pass.TypesInfo.ObjectOf(ident) // ident は*ast.Ident型 if obj == nil { continue } // obj.Type() でオブジェクトのTypeを得られる
3: エラーインターフェイスを満たしているなら、名前を事前に与えた正規表現にマッチするかを判定する: varErr.go#L73C41
2で得たobj.Nameが変数名の文字列なので、それと命名規則を表す正規表現のマッチを確認するだけです。
4: マッチしていなければ変数宣言の位置とともに報告する: varErr.go#L76
pass.Reportf()
に命名規則に違反している識別子が宣言された位置とメッセージを渡して出力します。
文章にして書くと長くなってしまっていますが、コードでは60行程度と短く、簡単なリンターであれば気軽に実装できることがわかりました。 なお、Antonboom/errnameはエラー型の配列などvarErrCheckerでは大雑把にしてしまっている部分についてもケアしているので、もう少し複雑な実装になっていました。
実装は、テスト用として用意したコードのASTを出力して眺めつつ処理を追ってみたり、標準のgo vetで呼ばれているリンターのコードを読んで書き方の参考にしたりして行いました。 説明が良くわからなくてもコードを読むとわかることがあるので、Goの標準パッケージのコードが読みやすかったりサンプルコードが多く含まれているのは助かりました。 特にwalk.go - GoはASTを深さ優先探索しているプログラムで参考になりました。
astパッケージに含まれる構造体については、https://monpoke1.hatenablog.com/entry/2018/12/16/110943 が日本語だとわかりやすかったです。