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に移動することなく、コードの問題を確認できるので、開発効率が上がる。