skeletonで始めるGoの静的解析

はじめに

メルペイのエキスパートチームの@tenntennです。Merpay Tech Openness Month 2022の4日目の記事です。本記事ではGoの静的解析ツールの開発を補助するskeletonというツールについて解説します。また、静的解析ツールの学習方法やGo Conferenceのメルカリブースで行うハンズオンについても触れます。

静的解析ツールとskeleton

Goにおける静的解析

Goが開発された目的の1つに開発ツールの作りやすさがあります(参考1参考2)。開発で使用するツールはソースコードを対象とした処理が必要になります。Goはソースコードを実行せずに解析する静的解析の機能を提供するgoパッケージが標準ライブラリとして用意されています。そのため、静的解析を用いたコードフォーマッタやLinterなどが作りやすい言語です。

標準ライブラリだけで構文解析をして抽象構文木(AST: Abstract Syntax Tree)を生成し、型チェックを行って型情報を取得することができます。標準ライブラリは言語仕様の変更に追従するため、Goのリリースと共にgoパッケージも最新の言語仕様に追従していきます。例えば、Go1.18でリリースされたジェネリクス(型パラメタ)にもgoパッケージは対応しています。「Online Spring Internship for Gophers 2022を開催しました」というブログでも触れたように、2022年3月に行ったインターンシップでも多くの学生がジェネリクスに関する静的解析ツールを作成してくれました。

静的解析ツールのモジュール化

標準ライブラリではないものの、Goチームが管理しているgolang.org/x/tools/goパッケージ(以下、x/tools/goと表記)においても静的解析に関連する機能を提供しています。x/tools/goパッケージを用いると、コールグラフや静的単一代入(SSA: Static Single Assignment)形式、ポインタ解析などが利用できます。

2018年に「Goにおける静的解析のモジュール化について」というブログに書いたように、x/tools/goパッケージでは静的解析ツールのモジュール化を行う機能を提供するanalysisパッケージをサブパッケージとして持ちます。analysisパッケージはGo1.12からgo vetコマンド(公式のLinter)でも利用されています。

静的解析ツール、特にソースコードからバグやコーディングスタイルをチェックするLinterは、複数の静的解析ツールを同じソースコード群に対して実行することが多いでしょう。analysisパッケージが登場する前は、次の図のように同じソースコードに対して似たような処理を実行していました。

analysisパッケージの登場で静的解析ツールをAnalyzerという単位で開発できるようになりました。Analyzerは次の図のように、同じソースコード群(パッケージ)に対して1度だけ実行されます。Analyzerは他のAnalyzerを使用でき、共通の処理はライブラリとして再利用できます。構文解析や型チェックなどのgoパッケージを使った共通処理はanalysisパッケージが自動で行ってくれます。

例えば、識別子(変数名や関数名など)がIdである箇所を見つけるLinterは次のように書けます。analysis.Analyzer構造体のRunフィールドの関数で作りたい静的解析ツールの処理を記述し、run関数のanalysis.Pass型の引数から構文解析や型チェック、依存するAnalyzerの解析結果などを取得します。analysis.Pass型のReportfメソッドを使用すると、指定したソースコードの位置にエラーがあることを報告できます。Analyzer構造体のRequiresフィールドで指定した依存するAnalyzerの解析結果は、analysis.Pass構造体のResultOfフィールドから取得できます。

var Analyzer = &analysis.Analyzer{
    Name: "findId",
    Doc:  "findId finds identifiers which name are Id",
    Run:  run,
    Requires: []*analysis.Analyzer{
        inspect.Analyzer, // 依存するAnalyzer
    },
}

func run(pass *analysis.Pass) (interface{}, error) {
    inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    nodeFilter := []ast.Node{ (*ast.Ident)(nil) }
    inspect.Preorder(nodeFilter, func(n ast.Node) {
        switch n := n.(type) {
        case *ast.Ident:
            if n.Name == "Id" {
                pass.Reportf(n.Pos(), "NG") // エラーを出力
            }
        }
    })

    return nil, nil
}

また、analysisパッケージはサブパッケージとしてanalysistestパッケージを提供しており、テストも簡単に作成できます。テスト対象となるソースコードと期待するLinterの出力結果をソースコードの該当箇所にコメントとして記述するだけでテストが可能です。

例えば、testdataディレクトリ以下に次のようなテストデータを置いてテストを行います。テストデータのソースコードには、エラーが起きることを期待する行にwantではじまるコメントを書きます。コメントを書いた行でwantの後に記述した正規表現にマッチするメッセージと共にエラーが出力されているかテストされます。

package a

func f() {
    var Id int  // want "NG"
    println(Id) // want "NG"
}

テストコードは次のようにanalysistest.Run関数にtestdataディレクトリへのパス、テストしたいAnalyzer、テストデータとして用いるtestdataディレクトリ以下に配置されたパッケージの名前を指定して行います。

func TestAnalyzer(t *testing.T) {
    testdata := analysistest.TestData()
    analysistest.Run(t, testdata, findId.Analyzer, "a")
}

analysisパッケージは実行可能ファイルを作成するために用いるパッケージもサブパッケージとして提供しています。次のようにunitcheckerパッケージのMain関数を用いるとgo vetコマンドから外部コマンド呼び出しで実行される実行可能ファイルを生成できます。Main関数には実行したいAnalyzerを可変長引数で渡します。

func main() {
    unitchecker.Main(findId.Analyzer)
}

go vetから-vettoolオプション経由で呼び出すことで解析対象のソースコードをパッケージ名から探し出す処理をgo vetが行います。例えば、次のようなコマンドを実行すると、カレントディレクトリ以下にあるすべてのパッケージに対してfindIdというAnalyzerを実行するという意味になります。なお、-vettoolオプションは実行したい静的解析ツールの実行可能ファイルへの絶対パスを指定する必要があるため、whichコマンドを使ってfindIdコマンドの絶対パスを指定しています。

$ go vet -vetool=`which findId` ./...

skeletonによる雛形の生成

analysisパッケージが提供する機能を使うと比較的簡単に静的解析ツール(Linter)が開発できます。analysis.Analyzer構造体のRunフィールドに設定する関数以外のほとんどは定形になっています。そこで筆者はskeletonというanalysisパッケージを用いた静的解析ツールの雛形を生成するコマンドラインツールを開発しました。

skeletonはgo installコマンドで最新バージョンをインストールできます。執筆時点での最新バージョンはv2.0.5です。

$ go install github.com/gostaticanalysis/skeleton/v2@latest

skeletonを用いると簡単に静的解析ツールの開発を始められます。例えば、次のようなコマンドを実行すると、用意する必要のあるソースコードのほとんどを自動生成します。

$ skeleton github.com/tenntenn/findId
$ tree findId
findId
├── cmd
│   └── findId
│       └── main.go
├── findId.go
├── findId_test.go
├── go.mod
└── testdata
    └── src
        └── a
            ├── a.go
            └── go.mod

5 directories, 6 files

静的解析ツールの開発者は、Analyzer構造体のRunフィールドに設定する関数を実装し、テストデータを用意するだけで静的解析ツールが作れます。デフォルトの設定では抽象構文木のノードを探索するようなサンプルコードが提供されます。

生成されるソースコードのテンプレートを変えたい場合は-kindオプションを利用します。-kindオプションにはinspect、ssa、codegen、packagesの4つが指定できます。inspectはデフォルトの設定です。ssaは静的単一代入形式を用いたサンプルコードを生成します。codegenとpackagesはanalysisパッケージを直接用いたサンプルコードではありませんが、analysisパッケージを用いた場合のソースコードと似たような構造でコードを生成します。codegenはgithub.com/gostaticanalysis/codegenパッケージを用いたコード生成に使用できるソースコードを生成します。また、pacakgesはgolang.org/x/tools/go/packagesパッケージを用いたLinter以外の静的解析ツールに使用できるソースコードを生成します。

skeletonが生成するコードは筆者が管理するgithub.com/gostaticanalysisオーガナイゼーションで提供されている便利なライブラリを使用しています。例えば、analysistestパッケージではGo Modulesで管理されたソースコードをテストデータとして用いることができませんが、代わりにgithub.com/gostaticanalysis/testutiパッケージのWithModules関数を利用すると良いでしょう。テストデータのパッケージが外部ライブラリに依存している場合に外部ライブラリのバージョンを固定できて便利です。また、testutil.RunWithVersions関数とtestutil.LatestVersion関数を組み合わせると、次のように最新から3バージョンを用いてテストを行うテスト関数が簡単に実装できます。

func TestAnalyzer(t *testing.T) {
    vers := LatestVersion(t, "github.com/tenntenn/greeting/v2", 3)
    testdata := analysistest.TestData()
    RunWithVersions(t, testdata, sample.Analyzer, vers, "a")
}

このように、skeletonを使うと簡単に静的解析ツールの開発を始められ、作りたいツールのメイン処理の実装に集中できます。

Goにおける静的解析を学習するには

プログラミング言語Go完全入門

筆者が提供しているプログラミング言語Go完全入門では、第14章で静的解析とコード生成を扱っています。300ページを超えるスライドで次のような項目を知ることができます。

  • 静的解析を行う理由
  • 静的解析クイックスタート
  • 構文解析
  • 型チェック
  • コード生成
  • 静的単一代入形式
  • パッケージ情報の取得
  • go/analysis詳細
  • コールグラフとポインタ解析
  • 型パラメタを含むコードの解析

Goにおける静的解析の基礎からコールグラフやポインタ解析などの高度な解析まで網羅的に知ることができます。また、Go1.18で導入されたジェネリクス(型パラメタ)を含むソースコードの静的解析についても扱っています。

公式のドキュメントには書かれていないような、筆者がソースコードを読んで調べた内部実装に関わる部分も記載されています。

Go Conference 2022 Spring

2022年4月23日(土)にGo Conference 2022 Spring Onlineが行われます。筆者も運営メンバーとして参加する予定です。メルカリとしてもシルバースポンサーとして支援しており、当日はオフィスアワー会場にメルカリブースを出展する予定です。メルカリブースでは「みんなも作ろう!Go Quiz 」と「初めての静的解析やってみよう!」という2つのコンテンツを提供する予定です。

「みんなも作ろう!Go Quiz 」はGo Quizと呼ばれるGoに関する知識を問うクイズを解くだけではなく、参加者も一緒に作りましょうという企画です。そのクイズでGoのどんな知識を問うのか、回答者がどういう思考で回答を選択するのか、などを考えて問題を作ることで作問者の知識も深まります。ぜひ、メルカリのメンバーと一緒に「あ、しまった。なるほどいい問題だ」と言われる難問を作り、他の参加者に体験してもらいましょう!

「初めての静的解析やってみよう!」では本稿で扱ったskeletonを用いて簡単な静的解析ツールを作成するハンズオンを行います。メンターは筆者と共に「逆引きGoによる静的解析入門」を執筆したknsh14が参加する予定です。これまで静的解析ツールを作ってみたかったけど、自分だけではなかなか始めるきっかけが無かった方もこれを機会に静的解析ツールを作ってみましょう。

まだconnpassで登録されていない方はぜひ登録しましょう。私は当日はメルカリのデザイナーでGopher部の部員でもあるtottieさんデザインのTシャツを着て参加します!

Go Conferenceはこの他にもコンテンツは盛りだくさんなので、みなさんもぜひ参加してみてください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加