ゴール
-
x/tools/go/analysis
package を使って、linter を実装できるようになる
背景
Go は言語本体の簡潔さ、強力さはもちろんのことですが、周辺のツール/環境が充実していることでも知られています。
linter についても、標準で go vet
や、標準の linter の runnner として、golangci-lint
というサードパーティのツールも利用されています。
さらに、Go ではそのような linter 、及び静的解析を実装するためのライブラリが準標準 (golang.org./x
以下)で公開されています。
今回は、そのライブラリを使ってユーザ独自の linter を作る方法を紹介します。
環境
- go1.14.4
- skeleton v1.2.1
gostaticanalysis/skeleton で linter のひな形を作る
go/analysis
package を使った linter の作成は、以下のツールでスケルトンを作成することができます。
https://github.com/gostaticanalysis/skeleton
skeleton が出力するスケルトンの内容
以下のようにファイル一式が出力されます。
.
│ hogefuga.go
│ hogefuga_test.go
│
├─cmd
│ └─hogefuga
│ main.go
│ varinit.exe
│
├─plugin
│ └─hogefuga
│ main.go
│
└─testdata
└─src
└─a
a.go
-
hogefuga.go/hogefuga_test.go
へ、linter の実装とテストを記載する -
cmd
以下はgo vet
等から利用する実行体を作成するコマンド -
plugin
以下はgolangci-lint
から利用するプラグインを作成するコマンド -
testdata
以下へhogefuga_test.go
から参照されるテストデータを作成する
これらで、linter を作るための基本的な材料は全てそろっていて、
すぐに linter の作成を始めることができます。
独自 linter を実装する
というわけで、早速 linter を作っていきます。
今回は簡単なサンプルとして、識別子に hoge/fuga のような意味のない名前を与えている場合、
それを linter の指摘として出力するというものを作ってみます。
テストデータを作成する
まず、linter の指摘を受けるような Go のコードをテストデータとして用意します。
go/analysis
package は、 go/analysis/analysistest
package を使って、テストができます。
そのテスト package では、 // want "期待値"
というコメントで、 その行にどんな linter の指摘を期待するかを記載してテストすることができます。
package a
import "fmt"
func a() {
var hoge string = "hoge" // want "identifier is meaningless"
fuga(hoge) // want "identifier is meaningless" "identifier is meaningless"
}
func fuga(s string) { // want "identifier is meaningless"
fmt.Println(s)
}
なお、テストコード側は以下のようになっており、複数 package を試すなどの場合除いては、
スケルトンの出力したコードからの変更は不要です。
package varinit_test
import (
"testing"
"github.com/akif999/varinit"
"golang.org/x/tools/go/analysis/analysistest"
)
// TestAnalyzer is a test for Analyzer.
func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, varinit.Analyzer, "a")
}
linter の処理を実装する
以下のように実装しました。
ユーザで書いているのは、ast node
を *ast.Ident
(識別子) へ絞り込むフィルタを作成していることと、
そのフィルタで ast を走査して、各 *ast.Ident
(識別子) の要素に対して、指摘を出力するかを判定します。
ちなみに、本記事では取り上げていませんが、指摘を出力するだけではなく suggest fix (= 修正案を提示) したり、
lint すると同時に修正したりということもできるようです。
package varinit
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
const doc = "varinit is ..."
// Analyzer is ...
var Analyzer = &analysis.Analyzer{
Name: "varinit",
Doc: doc,
Run: run,
Requires: []*analysis.Analyzer{
inspect.Analyzer,
},
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.Ident)(nil), // nodefileter の定義
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
switch n := n.(type) {
case *ast.Ident: // 識別子に対して処理を記載する
if isHoge(n.Name) || isFuga(n.Name) { // hoge、fuga に識別子が該当するかを確認する
pass.Reportf(n.Pos(), "identifier is meaningless") // 指摘ありの場合、指摘を出力する API をコールする
}
}
})
return nil, nil
}
func isHoge(s string) bool {
return s == "hoge"
}
func isFuga(s string) bool {
return s == "fuga"
}
独自 linter の動かし方
独自 linter は、 go vet から動かすことができます。
golangci-lint
の plugin を作ることができるようになっていますが、
記事執筆時点では、Windows/amd64 環境では build ができませんでした。
ちなみに、 go vet
本体の linter も、ある時点で go/analysis
package を使ってリライトされています。
C:\Users\aki01\work\src\github.com\akif999\hogefuga\cmd\hogefuga>go vet --vettool=hogefuga.exe ..\..\testdata\src\a
# _/C_/Users/aki01/work/src/github.com/akif999/hogefuga/testdata/src/a
..\..\testdata\src\a\a.go:6:6: identifier is meaningless
..\..\testdata\src\a\a.go:8:2: identifier is meaningless
..\..\testdata\src\a\a.go:8:7: identifier is meaningless
..\..\testdata\src\a\a.go:11:6: identifier is meaningless
まとめ
-
go/analysis package
を使うと、他の linter と共通の I/F で linter をつくることができる - linter の作成/テストをライブラリが支援してくれるので作るのが簡単
-
gostaticanalysis/skeleton
を使うとスケルトンを出力してくれて、更に作りやすい - 現場独自の linter を作って、自分たちのプロダクトの品質とスピードの向上に貢献しませんか??