Skip to content

Latest commit

 

History

History
344 lines (257 loc) · 13.6 KB

README_ja.md

File metadata and controls

344 lines (257 loc) · 13.6 KB

English Version

skeleton

skeletonはGoの静的解析ツールのためのスケルトンコードジェネレータです。x/tools/go/analysisパッケージやx/tools/go/packagesパッケージを用いた静的解析ツールの開発を簡単にします。

x/tools/go/analysisパッケージ

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のバージョンによってインストール方法が異なります。

Go1.16未満

$ go get -u github.com/gostaticanalysis/skeleton/v2

Go1.16以上

$ 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.goAnalyzer変数として定義されています。

生成されたコードは、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.Analyzergopher識別子をソースコードの中から探し報告します。テストでは、期待する報告をコメントで記述します。コメントは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ディレクトリを生成しない

-cmdオプションをfalseにするとcmdディレクトリは生成されません。

$ skeleton -cmd=false example.com/mylinter
mylinter
├── go.mod
├── mylinter.go
├── mylinter_test.go
└── testdata
    └── src
        └── a
            ├── a.go
            └── go.mod

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環境変数を指定するとモジュールパスの前にプリフィックスを付与します。

$ 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にした場合は親のモジュールのモジュールパスが使用されます。

singlecheckerまたはmulticheckerの使用

デフォルトではmain.goではgo vetから実行することを前提としたunitcheckerパッケージが使われています。-checkerオプションを指定することで、singlecheckerパッケージやmulticheckerパッケージに変更できます。

singlecheckerパッケージは、単一のAnalyzerを実行するためのパッケージでgo vetは必要としません。利用するには-change=singleを指定します。

multicheckerパッケージは、複数のAnalyzerを実行するためのパッケージでgo vetは必要としません。利用するには-change=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パッケージは実験的なパッケージです。ご注意ください。

golangci-lintのプラグインを生成する

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と生成した静的解析ツールで使用しているモジュールのバージョンを揃えないといけないため、あまりおすすめはしません。