golangci-lint に独自の Linter を統合する

お久しぶりです

ちゃんと元気に生きています。 1ヶ月ほど禁酒していて体の調子がすごく良いです。その代わりアイスの消費量が増えました。毎日ハーゲンダッツとかガリガリ君とか食べてます。

独自Linterをgolangci-lintと統合する手順

まずこの記事でカバーする範囲について補足します。

概要

プロジェクトで Linter として golangci-lint を使用している場合、他の Linter と合わせてプロジェクト独自の Linter を追加し適用させたい、という機会があるかもしれない。 ここでは golangci-lint 用の独自Linter を書いたあと、それを golangci-lint 上で使用できるようにするための手順をまとめた。

環境

本記事は以下の環境で動作確認した。なお golangci-lint のソースインストールが必要なため Windows 環境では動作しない可能性がある(手元に環境がないため検証できなくてすみません)。

ハマりどころ

  • Linter プラグインが依存するパッケージのバージョンをすべて golangci-lint と同一にしなければならない。
    • golangci-lint 本体のバージョンが変わると、プラグイン側の依存パッケージのバージョンも要変更になる可能性がある

大まかな説明の流れ

  • golangci-lint インストール
  • 独自 Linter パッケージを作成
  • プラグインファイルを生成
  • Linter を適用するプロジェクトの作成
  • Linter を適用

ディレクトリ構成

ホームディレクトリ以下に work という作業用ディレクトリを作成し、それ以下で作業を進めた。本記事の手順を試すにはまず任意の場所に空の work ディレクトリ(名前は何でも良い)を作成してから始めれば良い。最終的には以下のようなディレクトリ構成になる。

  • example-plugin-linter: プラグインのコード
  • hello: Linter の適用対象となるサンプルプロジェクト
  • plugins: golangci-lint に組み込み可能なプラグインを配置する場所
work
├── example-plugin-linter
│   ├── README.md
│   ├── example.go
│   ├── go.mod
│   ├── go.sum
│   ├── lint_test.go
│   ├── plugin
│   │   └── example.go
│   ├── testdata
│   │   └── src
│   │       └── testlintdata
│   │           └── todo
│   │               └── todo.go
│   └── tools.go
├── hello
│   ├── .golangci.yml
│   ├── go.mod
│   └── main.go
└── plugins
    └── example.so

準備

PATH$GOPATH/bin を追加しておく。

$ export PATH=$PATH:$GOPATH/bin

golangci-lint インストール

今回は v1.42.0 を使用する。 カスタム Linter プラグインgolangci-lint に組み込むには、本体をソースからインストールする必要がある(といっても go install を使ってインストールするだけ)。バイナリインストールでは Plugin の組み込みができないようだ。 インストール方法の具体的な解説は Install | golangci-lint に掲載されているが、以下の通りに実行すれば良い。 なお、gcc を必要に応じてインストールする。

$ go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.42.0

$ golangci-lint --version
golangci-lint has version v1.42.0 built from (unknown, mod sum: "h1:hqf1zo6zY3GKGjjBk3ttdH22tGwF6ZRpk6j6xyJmE8I=") on (unknown)

Linter プラグインの作成

今回は golangci/example-plugin-linter: example linter that can be used as a plugin for github.com/golangci/golangci-lint のサンプル Linter を使用する。

ちなみにこの LInter は、author の指定がない TODO: コメントを検知するためのもの。以下のような挙動をする。

// TODO: author がないので NG
// TODO(): author がないので NG
// TODO(dareka): これは author を指定しているからOK
main.go:5:1: todo: TODO comment has no author (example)
// TODO: author がないので NG
^
main.go:6:1: todo: TODO comment has no author (example)
// TODO(): author がないので NG
^

というわけで、Linter の構築を進める。

$ git clone https://github.com/golangci/example-plugin-linter.git
$ cd example-plugin-linter

このサンプルを元に、プラグインファイル(*.so)を生成する。

生成するコマンド自体は簡単。

$ go build -buildmode=plugin -o (出力先) (AnalyzerPlugin が定義された .go ファイル)

ただハマりどころなのは、このプラグインが依存するパッケージのバージョンを、golangci-lint 本体のそれと完全に合わせておく必要があるというだ。少しでもバージョンが異なると Linter を適用する段階でエラーとなる。

例えば、Linter のプラグインを書く時に不可欠な go/analysisgolang.org/x/tools パッケージに含まれるが、golangci-lint が依存している golang.org/x/tools のバージョンは、本体のバージョンによって変わる。

(golangci-lint v.1.41.1 の場合)

$ go version -m /home/egawata/go/bin/golangci-lint | grep golang.org/x/tools
    dep golang.org/x/tools  v0.1.3    h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8=

(golangcl-lint v1.42.0 の場合)

$ go version -m /home/egawata/go/bin/golangci-lint | grep golang.org/x/tools
    dep golang.org/x/tools  v0.1.5    h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=

そして、プラグイン側も golang.org/x/tools のバージョンが上記のものに合うよう、go.mod を調整しておく必要がある。

とはいえ、 go version -m $GOPATH/bin/golangci-lint を実行してすべてのパッケージのバージョンを確認し、go.mod 側を1つずつ書き換えていくのはあまり現実的ではない。そこで以下の方法で、確実にバージョンを合わせていく方法を取る。

(なお、この手順の意味については 参考1 が分かりすい)

golangci-lint パッケージをブランクインポート

tools.go というファイルをプラグインプロジェクトのルートに作成し、以下の内容を記述する。これにより、このプラグインプロジェクトが golangci-lint に依存することを宣言できる。もちろん実際に参照している箇所はないのでブランクインポートで良い。 (tools.go というファイル名や tools というパッケージ名は何でも良いのだが、慣習的に tools を使用するらしい)

// +build tools

package tools

import (
    _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
)

golangci-lint の依存パッケージのバージョンを go.mod に反映させる

いったん go.mod を捨てて作りなおす。 4行目の golangci-lint のバージョンには、インストールしたバージョンと同一のものを指定するところがポイント。 また go mod tidy と順番を逆にすると正しいバージョンのパッケージが入らないので注意。

$ rm go.*
$ go mod init example
$ go clean -modcache
$ go get -d github.com/golangci/golangci-lint/cmd/golangci-lint@v1.42.0
$ go mod tidy

プラグインファイルの出力

$ mkdir ../plugins     # なければ作る
$ go build -buildmode=plugin -o ../plugins/example.so plugins/example.go

-o に出力先を指定する。このパスはあとで設定に使用する。

Linter 適用対象のプロジェクトを作る

ここではサンプルプロジェクトとして hello というものを新規に作成する。もし既存のプロジェクトがあるならそれを利用してみても良い。

$ cd ~/work/
$ mkdir hello
$ cd hello
$ go mod init hello

この下に、チェック対象のソースコード(main.go)を以下のように作成する。 example Linter の検出対象となるよう、main() の前に問題のあるコメントを入れておく。

package main

import "fmt"

// TODO: implement here
func main() {
    fmt.Println("Hello")
}

Linter が example プラグインを使用するよう、 .golangci.yml に設定を追加する(なければ新規作成)

linters-settings:
  custom:
    example:
      path: ../plugins/example.so
      description: Check TODO without author
      original-url: github.com/golangci/example-linter

linters:
  enable:
    - example

linters-settings.custom 以下に Plugin 情報を設定する。pathプラグインの位置(絶対パス、もしくはhello プロジェクトからの相対パス)を設定する。 また linters.enable に、使用するプラグイン名を追加する。

Linter を適用

hello プロジェクトのルートディレクトリで golangci-lint 実行。正しく適用されていることが分かる。

$ golangci-lint run ./...
main.go:5:1: todo: TODO comment has no author (example)
// TODO: implement here
^

補足

ここでは、作成した独自 Linter を golangci-lintプラグインとして統合する方法を説明した。しかし今回の方法は、やや手順が煩雑だということのほかに懸念点が一つある。

それは golangci-lint をソースインストールする必要があるという点で、この方法はいくつか問題を抱えているようだ。

Install | golangci-lint

Note: such go get installation aren't guaranteed to work. We recommend using binary installation.

公式ドキュメントではその理由をいくつか挙げた上で、ソースインストールではなくバイナリインストールを推奨しているようだ。公式が推奨しない方法で利用を続けていくというのはいささか不安を拭えない。

独自 Linter を利用する方法がこれしかない、ということであれば素直に従う必要があるのだろうが、go vet から起動する など他のお手軽な方法もある。今回取り上げた方法はあくまで選択肢の一つとしてとらえるのが丁度良いのかもしれない。

参考サイト