Goの静的解析ツールを簡単に使うためのエコシステムについて考える #golang

December 25, 2019

はじめに

本稿はGo Advent Calendar 2019の25日目の記事です。

筆者は静的解析が大好きでオリジナルの静的解析ツールを作ったり、静的解析ネタで登壇したりしています。

Mercari Advent Calendar 2018の“Goにおける静的解析のモジュール化について”という記事をで書いたように、Goの静的解析ツールは再利用性を考え、モジュール化する流れになっています。

本稿では静的解析のモジュール化における課題と今後のエコシステムを考察します。

静的解析のモジュール化

モジュール化は、静的解析ツールをgolang.org/x/tools/go/analysisパッケージ(以下、analysisパッケージ)のAnalyzer構造体の単位に分け、それを再利用することで行います。 Analyzer構造体には、Requiresフィールドという依存するAnalyzerを設定するフィールドがあります。 1つのAnalyzerが複数のAnalyzerから依存されることはありますが、analysisパッケージが提供するエコシステムを利用すれば複数回解析が実行されることはありません。 そのため、できるだけ汎用的なAnalyzerを作ることで再利用性が高くなり、静的解析ツールの全体のパフォーマンスが上がることが期待できます。

golang.org/x/tools/go/analysis/unitcheckerパッケージを用いることで複数のAnalyzerをまとめてgo vetコマンドから実行することができます。

package main

import (
    "github.com/gostaticanalysis/dupimport"
    "github.com/gostaticanalysis/nilerr"
    "golang.org/x/tools/go/analysis/unitchecker"
)

func main() {
    unitchecker.Main(
        dupimport.Analyzer,
        nilerr.Analyzer,
    )
}

上記のコードをビルドし、mycheckerという名前のコマンドを作った場合、 以下のようにgo vetコマンドから-vettoolオプションで指定し呼び出せます。

$ go vet -vettool=`which mychecker`

モジュール化が生み出すエコシステム

analysisパッケージを用いた静的解析のモジュール化は静的解析ツールを開発する敷居を下げることに成功しています。 筆者が開発しているskeletonというツールを用いると、簡単にAnalyzerの雛形を作ることができます。

また、vetgenは、簡単にunitcheckerパッケージを用いた静的解析ツールを作れます。例えば、以下のように、vetgen addコマンドを呼び出すことでmain.goに任意のAnalyzerを追加できます。

$ vetgen add github.com/tenntenn/mychecker
$ cat main.go
// This file is generated by vetgen.
// Do NOT modified this file.
package main

// go vet
import (
	"golang.org/x/tools/go/analysis/unitchecker"
	"github.com/gostaticanalysis/vetgen/analyzers"
	"github.com/tenntenn/mychecker"     // add by vetgen
)

var myAnayzers = []*analysis.Analyzer {
	mychecker.Analyzer,
}

func main() {
	unitchecker.Main(append(
		analyzers.Recommend(),
		myAnayzers...,
	)
}

モジュール化の課題

モジュール化には多くの利点がありますが、課題が無いわけではありません。 モジュール化を行う単位やどこまでをAnalyzerの出力とするのかという点は非常に難しい問題です。

1つのAnalyzerをリッチにすることで便利にはなりますが、再利用性が下がります。 また、Analyzerの出力をリッチにしていくと、解析に時間がかかるようになります。 逆に出力を最小限にすると欲しい情報が得られなくなり、最終的には別のAnalyzerが誕生することになります。

例えば、静的単一代入形式(Static Single Assignment形式)を構築するようなAnalyzergolang.org/x/tools/go/analysis/passes/buildssaパッケージで提供されています。 静的単一代入形式といっても、どこまで情報を入れるかによって構築方法や結果が違います。

しかし、buildssaパッケージでは、静的単一代入形式の構築のモードを指定するフラグを固定しています。 そのため、buildssaパッケージで構築される静的単一代入形式では、情報が足りなくて利用することができない静的解析ツールもあるでしょう。 その場合は、別のモードで構築するようなAnalyzerを別途作成する必要がありますが、そのほとんどはbuildssaパッケージと同じになってしまうでしょう。

この他にもAnalyzer単位でモジュール化を行う上で実現が難しいことがあります。 各Analyzerに入力として渡される情報は、依存するAnalyzerの結果と抽象構文木(Abstract Syntax Tree: AST)、型情報になります。

構文解析や型チェックはパッケージ単位で行われるため、パッケージを跨いだような解析を行いたい場合はAnalyzer単位で行うには難しいでしょう。 例えば、テストコードを_testサフィックスをつけたパッケージ名で定義している場合は、テスト対象のコードともに解析することができません。 また、パッケージを跨いだコールグラフを取得したい場合も、入力となる静的単一代入形式がbuildssaパッケージを用いるとパッケージ単位でしか作られないため、完全なコールグラフを生成することはできません。

このように、現在のanalysisパッケージでは、どこまでを1つのAnalyzerにするのか、出力結果にどの情報を含めるかという問題やパッケージを跨いだ解析など課題が残ります。

Analyzerを見つける

analysisパッケージを用いた静的解析のモジュール化は、いくつかの問題を抱えながらも着実に浸透しているでしょう。 しかし、他の人が作ったAnalyzerを見つけるためにはどのように探せばよいでしょうか。

最近できたばかりのgo.devを用いると簡単にanalysisパッケージを使っているパッケージを見つけることができます。go.devにはパッケージを探したり詳細をみることのできる機能があります。

パッケージの詳細には、バージョンごとのGoDocやライセンス情報などの他に、どのパッケージからインポートされているかという情報も記載されています。

analysisパッケージの“Import By”タブを見ると、analysisパッケージをインポートしているパッケージの一覧が表示されます。 この一覧から辿ることでAnalyzerを見つけることが可能です。

しかし、analysisパッケージをインポートしているパッケージが必ずしもAnalyzerを提供しているとは限りません。 そのため、筆者はvetgenlistコマンドを導入することでAnalyzerの一覧を表示しようと考えました。

一通り完成はしたものの、毎回パッケージ単位で静的解析を行ってAnalyzer構造体を提供しているパッケージを検索しているため、恐ろしく時間がかかってしまいます。 サーバにキャッシュしたり、index.golang.orgの情報を元にインデックスを作るなどして対応できればと考えています。

おわりに

本稿では、analysisパッケージを用いたモジュール化の利点と、現在抱えている課題について書きました。 ぜひ、みなさんも自分オリジナルの静的解析ツールを作ってみてください。