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形式)を構築するようなAnalyzer
がgolang.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
を提供しているとは限りません。
そのため、筆者はvetgen
にlist
コマンドを導入することでAnalyzer
の一覧を表示しようと考えました。
一通り完成はしたものの、毎回パッケージ単位で静的解析を行ってAnalyzer
構造体を提供しているパッケージを検索しているため、恐ろしく時間がかかってしまいます。
サーバにキャッシュしたり、index.golang.orgの情報を元にインデックスを作るなどして対応できればと考えています。
おわりに
本稿では、analysis
パッケージを用いたモジュール化の利点と、現在抱えている課題について書きました。
ぜひ、みなさんも自分オリジナルの静的解析ツールを作ってみてください。