WHITEPLUS TechBlog

株式会社ホワイトプラスのエンジニアによる開発ブログです。

【Go】golangci-lintでカスタム静的解析を統合する

はじめに

Goの代表的な静的解析ツールには「go vet」「errcheck」「staticcheck」などがあります。

ホワイトプラスでは当初これらの静的解析ツールを個別でインストール・実行する方法を取っていました。

  • 当初の実行コマンドのイメージ
go vet ./...
go vet --vettool=nilness ./...
go vet --vettool=shadow ./...
errcheck --asserts ./...
staticcheck ./...

しかしこの手法ではパッケージのインストールがツールごとに必要だったり、それぞれの実行コマンドを追加する必要があったりと保守性・拡張性に難ありの状態でした。

そこで、これらの問題を解消すべくgolangci-lintを導入しました。

golangci-lintは複数の静的解析ツールをまとめて管理・実行してくれます。

(※設定・実行方法の詳細については後述します。)

  • golangci-lintの設定ファイル
linters:
  enable:
    - errcheck
    - govet
    - staticcheck
  • golangci-lintの実行コマンド
golangci-lint run ./...

golangci-lintを導入するメリットとして以下が挙げられます。

  • 複数の静的解析ツールを一括で管理・実行できる
  • 並列実行・キャッシュによる効率化
  • 柔軟なカスタマイズ性

また、Goではanalysisパッケージを使用してカスタムの静的解析ツールを作成することが可能です。

このカスタム静的解析をgolangci-lintに統合(既存パッケージと併せて管理・実行)することができれば、併せて効率性・保守性・開発生産性の向上が期待できます。

当記事ではこのカスタム静的解析をgolangci-lintに統合する方法について解説します。

golangci-lintにカスタム静的解析を統合する手順

golangci-lintにカスタム静的解析を導入する手順は大きく以下3つに分けられます。

  1. golangci-lintを導入する
  2. カスタム静的解析を作成する
  3. golangci-lintにカスタム静的解析を統合する

golangci-lintを導入する

まずはgolangci-lintのインストールから実行方法について説明します。1

インストール

以下コマンドでgolangci-lintをインストールします。

curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.62.2

バージョンが確認できればインストール完了です。

golangci-lint --version
> golangci-lint has version v1.62.2 

設定

インストールが完了したら、プロジェクトに.golangci.yml という名前の設定ファイルを用意します。

linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused

こちらのサンプルでは linters.enableerrcheckからunusedの6つの静的解析が有効化されています。

実行

設定ファイルを用意したら以下コマンドで静的解析が実行できます。

golangci-lint run ./...

エラーが検知された場合、以下のように出力されます。

main.go:7:6: func `foo` is unused (unused)
func foo() error {
     ^
make: *** [analyze] Error 1

main.goの7行目でunusedのエラーが検知されていることが分かります。

カスタム静的解析を作成する

最小構成の簡単なサンプルとして、「TODOコメントがあれば検知する」という静的解析を作成してみます。

最終的なディレクトリ構成は以下のようになります。

.
├── .golangci.yml
├── go.mod
├── go.sum
├── main.go
└── plugin
    └── analyser
        └── todo
            └── todo.go

まずtodo.goでカスタム静的解析の処理を実装します。

  • todo.go
package todo

import (
    "go/ast"
    "golang.org/x/tools/go/analysis"
    "strings"
)

var Analyzer = &analysis.Analyzer{
    Name: "todo",
    Doc:  "TODO コメントが含まれているか検出する",
    Run:  run,
}

func run(pass *analysis.Pass) (interface{}, error) {
    for _, file := range pass.Files {
        ast.Inspect(file, func(n ast.Node) bool {
            comment, ok := n.(*ast.Comment)
            if !ok {
                return true
            }
            if strings.Contains(comment.Text, "TODO") {
                pass.Reportf(comment.Pos(), "TODOコメントが含まれています")
            }
            return true
        })
    }
    return nil, nil
}

analysisパッケージのカスタム静的解析では、ファイルにAnalyzerという名前のパッケージ変数を用意するのが慣例となってます。

Analyzerrunフィールドに設定されているrun関数が静的解析実行時に処理されるようになります。

当記事ではカスタム静的解析の実装についての詳細は割愛しますが、analysisパッケージのドキュメントや以下記事が分かりやすくまとまっているので適宜参考にしてください。

zenn.dev

zenn.dev

続いて、上記で作成したAnalyzermain.goで呼び出すようにします。

  • main.go
package main

import (
    "my-app/plugin/analyser/todo"
    "golang.org/x/tools/go/analysis/unitchecker"
)

func main() {
    unitchecker.Main(todo.Analyzer) // Analzerを呼び出す
}

最後にmain.goをビルドして実行します。

  • ビルド
go build -o todo .
  • 実行

go vet でビルドしたファイルを実行できます。

go vet -vettool=todo ./...

試しに、エラーが検知されるように、main.goにTODOコメントを追加してみます。

package main

import (
    "my-app/plugin/analyser/todo"
    "golang.org/x/tools/go/analysis/unitchecker"
)

// TODO: ドキュメントを書く
func main() {
    unitchecker.Main(todo.Analyzer)
}

もう一度ビルド・実行してみるとエラーメッセージが出力されます。

# async-app
# [async-app]
./main.go:8:1: TODOコメントが含まれています

golangci-lintにカスタム静的解析を統合する

golangci-lintの導入とカスタム静的解析の作成が完了したので、最後にgolangci-lintにカスタム静的解析を統合します。

golangci-lintModule Pluginという仕組みを利用します。(参考

最終的なディレクトリ構成を示しておきます。

.
├── custom-gcl
├── go.mod
├── go.sum
├── main.go
└── plugin
    └── analyser
        ├── analyser.go
        ├── go.mod
        ├── go.sum
        └── todo
            └── todo.go

まず先ほど作成した静的解析をmain.goとは別のモジュールに切り出します。

cd plugin/analyser/
go mod init analyser

次に、analyzerモジュールをgolangci-lintに認識させるため、analyzer.goを用意します。

  • plugin/analyser/analyser.go
package analyser

import (
    "analyser/todo"
    "github.com/golangci/plugin-module-register/register"
    "golang.org/x/tools/go/analysis"
)

func init() {
    register.Plugin("analyser", New)
}

type plugin struct{}

func New(_ any) (register.LinterPlugin, error) {
    return &plugin{}, nil
}

func (p *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
    return []*analysis.Analyzer{
        todo.Analyzer,
    }, nil
}

func (p *plugin) GetLoadMode() string {
    return register.LoadModeSyntax
}

analyser.goでのポイントは以下2点です。

  • init()関数でanalyzerモジュールを登録
  • BuildAnalysers()関数で作成したカスタム静的解析を登録

続いて、.custom-gcl.ymlという名前の設定ファイルを用意します。

  • .custom-gcl.yml
version: v1.62.2
plugins:
  - module: 'analyser'
    path: ./plugin/analyser/

上記で作成したanalyserというモジュール名をmoduleに、analyserモジュールまでのパスをpathに指定します。

※注意: versionにはインストールしたgolangci-lintのバージョンを指定する必要があります。

さらに.golangci-lintに設定を追加します。

  • .golangci-yml
linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
linters-settings:
  custom:
    analyser:
      type: "module"

linters-settings.customに作成したモジュール名(analyzer)を追加し、typemoduleとします。

最後にビルドと実行です。

  • ビルド
golangci-lint custom

上記コマンドでcustom-gclというバイナリファイルが作成されます。

静的解析が実行されていることを確かめるためmain.goに細工しておきます。

  • main.go
package main

// TODO: ドキュメントを書く
func main() {}

// foo 未使用関数
func foo() error {
    return nil
}
  • 実行

作成されたcustom-gclを実行します。

./custom-gcl ./...
  • 出力
main.go:7:6: func `foo` is unused (unused)
func foo() error {
     ^
main.go:3:1: todo: TODOコメントが含まれています (analyser)
// TODO: ドキュメントを書く
^

作成したカスタム静的解析とunused静的解析が実行されていることが分かります。

まとめ

本記事では、golangci-lintで既存の静的解析ツールとカスタム静的解析を統合する方法を紹介しました。 一度上記の土台を作ってしまえば、一つのコマンドで静的解析が実行できて、追加したい静的解析がある場合の拡張性も高いので使い勝手がいいと思います。

さいごに

ホワイトプラスでは、ビジョンバリューに共感していただけるエンジニアを募集しています!
ネットクリーニングの「リネット」など、「生活領域×テクノロジー」で事業を展開しています。
弊社に興味がある方は、オウンドメディア「ホワプラSTYLE」をご覧ください。オンラインでのカジュアル面談も可能ですので、ぜひお気軽にお問い合わせください。


  1. golangci-lintのインストール方法や設定の詳細についてはドキュメントを参照してください。