この記事は、traPアドベントカレンダー2024 15日目の記事です。
こんにちは。ikura-hamu です。Go で Web アプリのサーバーを書いたりしています。先日(2024/12/8)に行われた ISUCON14 に出ていました(チームの参加ブログはこちら ISUCON14「リアクティブ二子玉川~♪」32位(学生5位) 参加記)。ISUCON14 に向けて作ったツールの中に、golangci-lint の Module Plugin を使ったものがあります。Module Plugin は比較的新しい機能でまだ記事が少なかったので、使い方や VSCode との統合の仕方などを書きたいと思います。
この記事でわかること
- 自作の静的解析ツールを golangci-lint に組み込んで使う方法
- 自作の静的解析ツールを VSCode で実行してエディタ画面に Warning を出す方法
説明しないこと
- golangci-lint とは何か
- golangci-lint の Go Plugin System について
目次
- golangci-lint の Module Plugin System とは
- Module Plugin やってみよう
- VSCode で Plugin の入った golangci-lint を使いたい
golangci-lint の Module Plugin System とは
golangci-lint の v1.57.0 で追加された、カスタムの Linter を golangci-lint で実行するための仕組みです。
公式ドキュメント: https://golangci-lint.run/plugins/module-plugins/
ざっくり説明すると、自分が作った Linter を追加した golangci-lint のバイナリをビルドするための仕組みです。以前も Go Plugin System という自作 Linter を組み込むための仕組みはありましたが、CGO が必要だったり、 golangci-lint と依存するライブラリのバージョンを全て揃える必要があったりと、かなり面倒な仕組みでした。それに比べて Module Plugin System はお手軽に自作の Linter を追加することができます。
Module Plugin やってみよう
実際に Module Plugin を使ってみます。
このリポジトリでやりました。参考にしてください。
https://github.com/ikura-hamu/golangci-lint-module-plugin-example
Linter を作る
今回は識別子がgopher
のものを探すシンプルな linter を作りました。
前提として、golang.org/x/tools/go/analysis.(*Analyzer)
を使った静的解析ツールである必要があります。
コードを見る
example.gopackage golangci_lint_module_plugin_example
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 = "gopher finds identifiers named gopher"
// Analyzer is ...
var Analyzer = &analysis.Analyzer{
Name: "gopher",
Doc: doc,
Run: run,
Requires: []*analysis.Analyzer{
inspect.Analyzer,
},
}
func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.Ident)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
switch n := n.(type) {
case *ast.Ident:
if n.Name == "gopher" {
pass.Reportf(n.Pos(), "identifier is gopher")
}
}
})
return nil, nil
}
go vet
で使うのであれば、この状態でも使えます。
go build -o example cmd/golangci_lint_module_plugin_example/main.go
go vet -vettool=./example testdata/src/a/a.go
output# command-line-arguments
testdata/src/a/a.go:5:6: identifier is gopher
testdata/src/a/a.go:6:8: identifier is gopher
gopher
という識別子を見つけることができました。これを golangci-lint に組み込んでいきます。
plugin にする
Module Plugin として使うために、plugin.go
を作ります。(ファイル名は何でも可)
plugin.gopackage golangci_lint_module_plugin_example
import (
"github.com/golangci/plugin-module-register/register"
"golang.org/x/tools/go/analysis"
)
func init() {
register.Plugin("gopher", New)
}
func New(settings any) (register.LinterPlugin, error) {
// The configuration type will be map[string]any or []interface, it depends on your configuration.
// You can use https://github.com/go-viper/mapstructure to convert map to struct.
return &plugin{}, nil
}
type plugin struct{}
var _ register.LinterPlugin = new(plugin)
func (*plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
return []*analysis.Analyzer{
Analyzer,
}, nil
}
func (*plugin) GetLoadMode() string {
return register.LoadModeSyntax
}
このコードで大事なのは、init
関数で呼び出される register.Plugin("gopher", New)
です。
Module Plugin System では、自作の linter 含むパッケージを golangci-lint 側で blank import (import _ "package path"
みたいにするやつ)することでプラグインを追加します。パッケージが import されるとまずinit
関数が実行されます。ここでプラグインを登録しているわけです。
register.Plugin()
の型は、func Plugin(name string, p NewPlugin)
です。NewPlugin
はtype NewPlugin func(conf any) (LinterPlugin, error)
のように定義されています。つまり、register.Plugin()
には、プラグイン名と設定を受け取ってLinterPlugin
を返す関数を渡せばよいです。
LinterPlugin
は interface になっているので、空の struct に対して必要なメソッドを実装します。
BuildAnalyzers
はプラグイン化したいAnalyzerを返すようにします。
GetLoadMode
は返り値をLoadModeSyntax
かLoadModeTypesInfo
から選びます。この二つの違いはあまりドキュメントには書かれていませんが、構文解析までしか行わないか、型チェックまで行うかを指定することになります。今回は識別子を探すだけで型情報は必要ではないので、LoadModeSyntax
を指定します。
plugin.go
を書いたら、次に.custom-gcl.yml
という Module Plugin System のための設定ファイルを作成します。
.custom-gcl.ymlversion: v1.62.2
plugins:
- module: "github.com/ikura-hamu/golangci_lint_module_plugin_example"
path: "."
version
は今入っている golangci-lint のバージョンではなく、プラグインを入れたい golangci-lint のバージョンを指定します。v1.57.0 以降である必要があります。
plugins
の項目でプラグインを設定します。GitHub などで公開されていて Go Proxy から取得できる場合と、手元にあるコードを使う場合の2種類の方法があります。今回は手元にあるコードを使います。module
にはプラグインのモジュール名を、path
には先ほどplugin.go
を置いたディレクトリへのパス(相対パス・絶対パスともに可)を指定します。
(Go Proxy を使った設定方法は公式ドキュメントを確認してください。)
他にも生成するバイナリ名なども指定できます。
最後にバイナリファイルをビルドします。
golangci-lint custom
custom-gcl
というバイナリができるはずです。
./custom-gcl --version
と実行すると、
outputgolangci-lint has version v1.62.2-custom-gcl built with go1.23.0 from ? on 2024-12-11 13:49:49.131175876 +0000 UTC
のように出力されます。いかにもカスタムされてそうですね。
plugin を使う
golangci-lint には linters
という Linter 一覧を表示してくれるコマンドがあります。カスタムしたもので実行して、追加したgophers
という Linter が含まれているか確かめてみましょう。
./custom-gcl linters | grep gopher
見つからないはずです。実際にプラグインとして使うには、.golangci.yml
に設定を書き足す必要があります。
.golangci.ymllinters-settings:
custom:
gopher:
type: module
linters:
disable-all: true
enable:
- gopher
linters-settings
の custom
という項で、type: module
とすることで Module Plugin であることを示しています。ここでは Linter の説明なども書くことができますが、今回は省略します。
linters
では他の組み込みの Linter と同様に、有効化するための設定を書きます。今回はわかりやすくするためにdisable-all: true
として、他の Linter を無効化しておきます。
この状態でもう一度 Linter 一覧を確認してみましょう。
./custom-gcl linters
outputEnabled by your configuration linters:
gopher: [fast: true, auto-fix: false]
...
有効になっていることが確認できました。テストデータに対して custom-gcl
を実行してみましょう。
./custom-gcl run testdata/src/a/a.go
outputtestdata/src/a/a.go:5:6: identifier is gopher (gopher)
var gopher int // want "identifier is gopher"
^
testdata/src/a/a.go:6:8: identifier is gopher (gopher)
print(gopher) // want "identifier is gopher"
^
動作していることが確認できました!
VSCode で Plugin の入った golangci-lint を使いたい
ここまででCLIによる実行はできるようになりましたが、せっかくなら VSCode などのエディタ上でもエラーを表示したいです。ここからは VSCode での設定をしていきます。簡単です。他のエディタでも同様の設定があると思うので、調べてみてください。
パスの通った場所に置く
まずはバイナリをパスが通ったお好みの場所に移動します。
VSCode が見る golangci-lint のファイル名を置き換える
.vscode/settings.json
に以下のような設定を書きます。
.vscode/settings.json{
"go.lintTool": "golangci-lint",
"go.alternateTools": {
"golangci-lint": "custom-gcl",
}
}
"go.lintTool": "golangci-lint"
は見慣れた設定だと思いますが、VSCode で使う Linter を golangci-lint にするというものです(デフォルトはstatickchech
)。
go.alternateTools
では、VSCode の Go 拡張機能が使うツールを置き換えることができます。今回の設定では、golangci-lint
を custom-gcl
に置き換えています。
設定ファイルを書き換えたら、VSCode をリロードしましょう。
エディタ上に警告を表示することができました!
おわり
自分の作ったツールが VSCode 上で動いていると楽しいです。
今回は手元でビルドを行いましたが、GitHub Actions などでビルドを行い、プラットフォームごとにバイナリをダウンロードできるようにするとチーム内で共有しやすくていいかもしれませんね。
Go は静的解析ツールを作るための環境が充実しているので、みなさんも作りましょう。
明日のアドベントカレンダー担当は @Takeno_hito、@cp20 の2人です。