2025/06/03
はじめに
Goで静的解析をしたい場合、golang.org/x/tools/go/analysisを使うと実現できる。
これを multichecker.Main()などで実行すると、CLIからLintを実行できる。
しかし、エディタとの親和性のために、golangci-lintにカスタムLinterを取り込んで、それをエディタ上で実行する方法を紹介する。
ここで紹介するのは、自作Linterをgolangci-lintのプラグインとして取り込み、それを用いてプラグインとして実行したときの知見である。
前提
まず、golangci-lintに取り込みたい&analysis.Analyzerはすでに存在するものとする。
Analyzerのサンプル
今回はまずサンプルの定義とし、処理の目的の SafetyAnalyzerと、これを(他のAnalyzerと)統合する ExampleAnalyzerという2段構成で書いておく。
package example_plugin
import (
"flag"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SafetyAnalyzer = &analysis.Analyzer{
Name: "safety_analyzer",
Doc: "",
URL: "",
Flags: flag.FlagSet{Usage: func() {}},
Run: nil, // 実際には `func run(pass *analysis.Pass) (interface{}, error)` を入れる
RunDespiteErrors: false,
Requires: []*analysis.Analyzer{inspect.Analyzer},
ResultType: nil,
FactTypes: []analysis.Fact{},
}
var ExampleAnalyzer = &analysis.Analyzer{
Name: "example_analyzer",
Doc: "",
URL: "",
Flags: flag.FlagSet{Usage: func() {}},
Run: run,
RunDespiteErrors: false,
Requires: []*analysis.Analyzer{inspect.Analyzer},
ResultType: nil,
FactTypes: []analysis.Fact{},
}
func run(pass *analysis.Pass) (interface{}, error) {
if _, err := SafetyAnalyzer.Run(pass); err != nil {
return nil, err
}
return nil, nil
}
pluginを実装
golangci-lintのプラグインは、github.com/golangci/plugin-module-register/registerを使うことで記述可能。
init()を使うことで、コードが読み込まれた時にpluginがregisterされるようにする。
Settingsは自由に設定でき、.golangci.ymlから設定を入れることができるようになる。
package example_plugin
import (
"golang.org/x/tools/go/analysis"
"github.com/golangci/plugin-module-register/register"
)
func init() {
register.Plugin("example", New)
}
type Settings struct {
Enable bool `json:"enable"`
}
type ExamplePlugin struct {
settings Settings
}
var _ register.LinterPlugin = &ExamplePlugin{}
func New(input any) (register.LinterPlugin, error) {
settings, err := register.DecodeSettings[Settings](input)
if err != nil {
return nil, err
}
return &ExamplePlugin{
settings: settings,
}, nil
}
func (ep *ExamplePlugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
analyzers := []*analysis.Analyzer{}
if !ep.settings.Enable {
return analyzers, nil
}
return []*analysis.Analyzer{
ExampleAnalyzer,
}, nil
}
func (ep *ExamplePlugin) GetLoadMode() string {
return register.LoadModeSyntax
}
pluginをビルド
上述の実装をしたあと、.custom-gcl.ymlを作成し、
version: v1.61.0
name: golangci-lint
destination: .
plugins:
- module: 'github.com/<username>/<reponame>'
import: 'github.com/<username>/<reponame>/pkg/example_plugin'
path: '..'
というふうにビルド設定を記述する。各フィールドは各々の環境に合わせて書き換える。
その後、環境にインストールしてあるgolangci-lintを使ってビルドする。
$ golangci-lint custom
と実行すると、 destinationで指定したディレクトリに、プラグインが配置される。
.golangci.ymlの設定
実際にLinterで有効化するには、以下のようにlinters設定とlinters-settings.custom設定を記述する。 settingsは、うえで定義したSettings構造体に対応する。
linters-settings:
custom:
example:
type: 'module'
settings:
# ここに設定を記述
linters:
enable:
- example
ビルド環境の依存を減らす
上の手法では、ビルド環境にgolangci-lintがインストールされている必要がある。
しかし、プラグイン付きでビルドする場合、インストールしてあるgolangci-lintはすぐに使わなくなるので、インストールしないで使いたいので、そのための手法も準備したので、紹介しておく。
こちらは正式な手法じゃなさそうなので、互換性など含め、あくまで自己責任で!!
以下のように、golangci-lintが公開している、コマンド実行のためのパッケージを使う。
package main
import (
"fmt"
"os"
"github.com/golangci/golangci-lint/pkg/commands"
)
func main() {
os.Args = []string{"", "custom"} // dummy args for building custom linter
if err := commands.Execute(commands.BuildInfo{
GoVersion: "",
Version: "",
Commit: "",
Date: "",
}); err != nil {
fmt.Printf("error: %v", err)
}
}
おわりに
エディタでカスタムAnalyzerを実行できると、CLIに移動することなく、コードの問題を確認できるので、開発効率が上がる。