feature image

2024年12月15日 | ブログ記事

おなかが空いたら golangci-lint の Module Plugin 使おう

この記事は、traPアドベントカレンダー2024 15日目の記事です。

こんにちは。ikura-hamu です。Go で Web アプリのサーバーを書いたりしています。先日(2024/12/8)に行われた ISUCON14 に出ていました(チームの参加ブログはこちら ISUCON14「リアクティブ二子玉川~♪」32位(学生5位) 参加記)。ISUCON14 に向けて作ったツールの中に、golangci-lint の Module Plugin を使ったものがあります。Module Plugin は比較的新しい機能でまだ記事が少なかったので、使い方や VSCode との統合の仕方などを書きたいと思います。

この記事でわかること

説明しないこと

目次

golangci-lint の Module Plugin System とは

golangci-lint の v1.57.0 で追加された、カスタムの Linter を golangci-lint で実行するための仕組みです。

公式ドキュメント: https://golangci-lint.run/plugins/module-plugins/

Module Plugin System | golangci-lint
Fast Go linters runner golangci-lint.

ざっくり説明すると、自分が作った 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

GitHub - ikura-hamu/golangci-lint-module-plugin-example
Contribute to ikura-hamu/golangci-lint-module-plugin-example development by creating an account on GitHub.

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
}

コミット: https://github.com/ikura-hamu/golangci-lint-module-plugin-example/tree/2d92632883337188570ae54201b14dc1cf4c32d5

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 を作ります。(ファイル名は何でも可)

コミット: https://github.com/ikura-hamu/golangci-lint-module-plugin-example/tree/72a33c97d8fc14b796630c5dbc2fd775c9f0fdf1

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)です。NewPlugintype NewPlugin func(conf any) (LinterPlugin, error)のように定義されています。つまり、register.Plugin()には、プラグイン名と設定を受け取ってLinterPluginを返す関数を渡せばよいです。

LinterPluginは interface になっているので、空の struct に対して必要なメソッドを実装します。

BuildAnalyzersはプラグイン化したいAnalyzerを返すようにします。
GetLoadModeは返り値をLoadModeSyntaxLoadModeTypesInfoから選びます。この二つの違いはあまりドキュメントには書かれていませんが、構文解析までしか行わないか、型チェックまで行うかを指定することになります。今回は識別子を探すだけで型情報は必要ではないので、LoadModeSyntaxを指定します。

plugin.goを書いたら、次に.custom-gcl.ymlという Module Plugin System のための設定ファイルを作成します。

コミット: https://github.com/ikura-hamu/golangci-lint-module-plugin-example/tree/b39c94de8fcb72a028f835d2e4537fd902f54b59

.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に設定を書き足す必要があります。

コミット: https://github.com/ikura-hamu/golangci-lint-module-plugin-example/tree/df5eecc970f0859e1f4e751f74eafb63342a0981

.golangci.ymllinters-settings:
  custom:
    gopher:
      type: module

linters:
  disable-all: true
  enable:
    - gopher

linters-settingscustom という項で、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 のファイル名を置き換える

コミット: https://github.com/ikura-hamu/golangci-lint-module-plugin-example/tree/dfd4022a365de1353f6db53785021fc5614f2229

.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-lintcustom-gcl に置き換えています。

設定ファイルを書き換えたら、VSCode をリロードしましょう。

----------2024-12-12-001658

エディタ上に警告を表示することができました!

おわり

自分の作ったツールが VSCode 上で動いていると楽しいです。
今回は手元でビルドを行いましたが、GitHub Actions などでビルドを行い、プラットフォームごとにバイナリをダウンロードできるようにするとチーム内で共有しやすくていいかもしれませんね。

Go は静的解析ツールを作るための環境が充実しているので、みなさんも作りましょう。

明日のアドベントカレンダー担当は @Takeno_hito@cp20 の2人です。

ikura-hamu icon
この記事を書いた人
ikura-hamu

SysAd班、ゲーム班 いろいろやりたい

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2024年6月21日
ハッカソン参加記 4班"Slide Center"
Alt--er icon Alt--er
2024年3月15日
個人開発として2週間でWebサービスを作ってみた話 〜「LABEL」の紹介〜
Natsuki icon Natsuki
2023年10月20日
DIGI-CON HACKATHON 参加記事「Comic DoQ」
mehm8128 icon mehm8128
2023年6月23日
2023 春ハッカソン 26班 『traP Mission』
Ras icon Ras
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記