ys memos
Blog

golangci-lintにカスタムAnalysisを取り込んでビルド


golang

2025/06/03


Goで静的解析をしたい場合、golang.org/x/tools/go/analysisを使うと実現できる。

これを multichecker.Main()などで実行すると、CLIからLintを実行できる。 しかし、エディタとの親和性のために、golangci-lintにカスタムLinterを取り込んで、それをエディタ上で実行する方法を紹介する。

ここで紹介するのは、自作Linterをgolangci-lintのプラグインとして取り込み、それを用いてプラグインとして実行したときの知見である。


まず、golangci-lintに取り込みたい&analysis.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
}

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
}

上述の実装をしたあと、.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で指定したディレクトリに、プラグインが配置される。


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


関連タグを探す