skeletonはGoの静的解析ツールのためのスケルトンコードジェネレータです。x/tools/go/analysisパッケージやx/tools/go/packagesパッケージを用いた静的解析ツールの開発を簡単にします。
x/tools/go/analysisパッケージは静的解析ツールをモジュール化するためのパッケージです。analysis.Analyzer型を1つの単位として扱います。
x/tools/go/analysis
パッケージは、静的解析ツールの共通部分を定型化しています。skeletonは定型化されているコードの大部分をスケルトンコードとして生成します。skeleton mylinter
コマンドを実行するだけで*analyzer.Analyzer
型の初期化コードやテストコード、go vet
から実行できる実行可能ファイルを作るためのmain.go
を生成してくれます。
skeletonについて詳しく知りたい場合は、次のブログも参考になります。
x/tools/go/analysis
パッケージやGoの静的解析自体を知りたい場合は、次の資料が参考になります。
$ go install github.com/gostaticanalysis/skeleton/v2@latest
skeletonの引数にモジュールパスを指定するとそのパスでモジュールを生成します。ディレクトリ名はモジュールパスの最後の要素になります。example.com/mylinter
と指定すると次のようになります。
$ skeleton example.com/mylinter
mylinter
├── cmd
│ └── mylinter
│ └── main.go
├── go.mod
├── mylinter.go
├── mylinter_test.go
└── testdata
└── src
└── a
├── a.go
└── go.mod
x/tools/go/analysis
パッケージを用いて開発された静的解析ツールは、*analysis.Analyzer
型の値として表現されます。mylinterの場合、mylinter.go
にAnalyzer
変数として定義されています。
生成されたコードは、inspect.Analyzer
を用いた簡単な静的解析ツールを実装しています。この静的解析ツールは、gopher
という名前の識別子を見つけるだけです。
package mylinter
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
const doc = "mylinter is ..."
// Analyzer is ...
var Analyzer = &analysis.Analyzer{
Name: "mylinter",
Doc: doc,
Run: run,
Requires: []*analysis.Analyzer{
inspect.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 == "gopher" {
pass.Reportf(n.Pos(), "identifier is gopher")
}
}
})
return nil, nil
}
skeletonは、テストコードも生成します。x/tools/go/analysis
パッケージはサブパッケージのanalysistest
パッケージとして、テストライブラリを提供しています。analysistest.Run
関数はtestdata/src
ディレクトリ以下にあるソースコードを使ってテストを実行します。この関数の第2引数はテストデータのディレクトリです。第3引数はテスト対象のAnalyzer、第4引数以降はテストデータとして利用するパッケージ名です。
package mylinter_test
import (
"testing"
"github.com/gostaticanalysis/example.com/mylinter"
"github.com/gostaticanalysis/testutil"
"golang.org/x/tools/go/analysis/analysistest"
)
// TestAnalyzer is a test for Analyzer.
func TestAnalyzer(t *testing.T) {
testdata := testutil.WithModules(t, analysistest.TestData(), nil)
analysistest.Run(t, testdata, mylinter.Analyzer, "a")
}
mylinterの場合、テストはtestdata/src/a/a.go
ファイルをテストデータとして利用します。mylinter.Analyzer
はgopher
識別子をソースコードの中から探し報告します。テストでは、期待する報告をコメントで記述します。コメントはwant
で始まり、その後に期待するメッセージが正規表現で記述されます。テストは期待するメッセージで報告がされなかったり、期待していない報告がされた場合に失敗します。
package a
func f() {
// The pattern can be written in regular expression.
var gopher int // want "pattern"
print(gopher) // want "identifier is gopher"
}
デフォルトではgo mod tidy
コマンドとgo test
コマンドを実行すると、テストは失敗します。これはpattern
というメッセージで作った静的解析ツールが報告をしないためです。
$ go mod tidy
go: finding module for package golang.org/x/tools/go/analysis
go: finding module for package github.com/gostaticanalysis/testutil
go: finding module for package golang.org/x/tools/go/analysis/passes/inspect
go: finding module for package golang.org/x/tools/go/analysis/unitchecker
go: finding module for package golang.org/x/tools/go/ast/inspector
go: finding module for package golang.org/x/tools/go/analysis/analysistest
go: found golang.org/x/tools/go/analysis in golang.org/x/tools v0.1.10
go: found golang.org/x/tools/go/analysis/passes/inspect in golang.org/x/tools v0.1.10
go: found golang.org/x/tools/go/ast/inspector in golang.org/x/tools v0.1.10
go: found golang.org/x/tools/go/analysis/unitchecker in golang.org/x/tools v0.1.10
go: found github.com/gostaticanalysis/testutil in github.com/gostaticanalysis/testutil v0.4.0
go: found golang.org/x/tools/go/analysis/analysistest in golang.org/x/tools v0.1.10
$ go test
--- FAIL: TestAnalyzer (0.06s)
analysistest.go:454: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern`
analysistest.go:518: a/a.go:5: no diagnostic was reported matching `pattern`
FAIL
exit status 1
FAIL github.com/gostaticanalysis/example.com/mylinter 1.270s
skeletonはcmd
ディレクトリ以下にmain.go
も生成します。このmain.go
をビルドし生成した実行可能ファイルは、go vet
コマンド経由で実行される必要があります。go vet
コマンドの-vettool
オプションは生成した実行可能ファイルへの絶対パスを指定します。
$ go vet -vettool=`which mylinter` ./...
すでにディレクトリが存在する場合は上書きするか聞かれます。
$ skeleton example.com/mylinter
mylinter is already exist, overwrite?
[1] No (Exit)
[2] Remove and create new directory
[3] Overwrite existing files with confirmation
[4] Create new files only
選んだ選択肢によって処理が変わります。
- [1] 上書きしない(終了)
- [2] 削除して新しいディレクトリを作成
- [3] すでにあるファイルを上書きするか都度確認
- [4] 新しいファイルのみ生成する
-cmd
オプションをfalse
にするとcmd
ディレクトリは生成されません。
$ skeleton -cmd=false example.com/mylinter
mylinter
├── go.mod
├── mylinter.go
├── mylinter_test.go
└── testdata
└── src
└── a
├── a.go
└── go.mod
skeletonはデフォルトではgo.mod
ファイルを生成します。すでにGo Modules管理下にあるディレクトリでスケルトンコードを生成したい場合は、次のように-gomod
オプションにfalse
を指定します。
$ skeleton -gomod=false example.com/mylinter
mylinter
├── cmd
│ └── mylinter
│ └── main.go
├── mylinter.go
├── mylinter_test.go
└── testdata
└── src
└── a
├── a.go
└── go.mod
次のようにSKELETON_PREFIX
環境変数を指定するとモジュールパスの前にプリフィックスを付与します。
$ SKELETON_PREFIX=example.com skeleton mylinter
$ head -1 mylinter/go.mod
module example.com/mylinter
次のようにdirenvなどを用いて特定のディレクトリ以下でプリフィックスをつけるようにすると便利です。
$ cat ~/repos/gostaticanalysis/.envrc
export SKELETON_PREFIX=github.com/gostaticanalysis
SKELETON_PREFIX
環境変数を指定していても、-gomod
オプションをfalse
にした場合は親のモジュールのモジュールパスが使用されます。
デフォルトではmain.go
ではgo vet
から実行することを前提としたunitchecker
パッケージが使われています。-checker
オプションを指定することで、singlechecker
パッケージやmultichecker
パッケージに変更できます。
singlechecker
パッケージは、単一のAnalyzerを実行するためのパッケージでgo vet
は必要としません。利用するには-checker=single
を指定します。
multichecker
パッケージは、複数のAnalyzerを実行するためのパッケージでgo vet
は必要としません。利用するには-checker=multi
を指定します。
次にsinglechecker
パッケージを利用した例を示します。
$ skeleton -checker=single example.com/mylinter
$ cat cmd/mylinter/main.go
package main
import (
"mylinter"
"golang.org/x/tools/go/analysis/singlechecker"
)
func main() { singlechecker.Main(mylinter.Analyzer) }
singlechecker
パッケージやmultichecker
パッケージを利用した方が簡単そうに見えますが、go vet
を使った恩恵を受けられないため、特にこだわりがない場合はunitchecker
(デフォルト)を使用すると良いでしょう。
skeletonは-kind
オプションを指定することで生成するスケルトンコードを変更できます。
-kind=inspect
(デフォルト):inspect.Analyzer
を用いたコードを作成-kind=ssa
:buildssa.Analyzer
で生成した静的単一代入(SSA, Static Single Assignment)形式を用いたコードを作成-kind=codegen
: コード生成器を作成-kind=packages
:x/tools/go/packages
パッケージを用いたコードを作成
skeletonは-kind
オプションにcodegen
を指定するとgostaticanalysis/codegenパッケージを用いたコード生成器のスケルトンコードも生成できます。
$ skeleton -kind=codegen example.com/mycodegen
mycodegen
├── cmd
│ └── mycodegen
│ └── main.go
├── go.mod
├── mycodegen.go
├── mycodegen_test.go
└── testdata
└── src
└── a
├── a.go
├── go.mod
└── mycodegen.golden
gostaticanalysis/codegen
パッケージは実験的なパッケージです。ご注意ください。
skeletonは-plugin
パッケージを指定するとgolangci-lintからプラグインとして利用できるコードを生成します。
$ skeleton -plugin example.com/mylinter
mylinter
├── cmd
│ └── mylinter
│ └── main.go
├── go.mod
├── mylinter.go
├── mylinter_test.go
├── plugin
│ └── main.go
└── testdata
└── src
└── a
├── a.go
└── go.mod
ビルド方法はgolangci-lintのドキュメントにも記載がありますが、生成されたコードの先頭にコメントとして記述されています。
$ skeleton -plugin example.com/mylinter
$ go build -buildmode=plugin -o path_to_plugin_dir example.com/mylinter/plugin/mylinter
もし、プラグインで特定のフラグを指定したい場合は、ビルドする際に-ldflags
オプションを指定して設定します。この機能はskeletonで生成したコードのみに提供されます。詳しくは生成されたスケルトンコードをご覧ください。
$ skeleton -plugin example.com/mylinter
$ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/mylinter/plugin/mylinter
なお、プラグインは標準のplugin
パッケージを使用するため、golangci-lintをCGO_ENABLED=1
でビルドし直す必要があります。また、golangci-lintと生成した静的解析ツールで使用しているモジュールのバージョンを揃えないといけないため、あまりおすすめはしません。