🔥

Go言語で静的解析をやってみる

2023/02/14に公開

はじめに

Gopher塾 #3 に参加し、静的解析について学びました。
やったことを忘れないため & アウトプットのためにまとめたいと思います。
実際に行った流れの通りに説明しようと思います。

Gopher塾 #3
https://tenntenn.connpass.com/event/271533/

今回作成した静的解析コード
https://github.com/AbeTetsuya20/gopher-3

静的解析の始め方

skeletonをインストールします。

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

skeletonを実行します。

skeleton myanalyzer

以下のようなディレクトリで自動生成されます。

mylinter
├── cmd
│   └── mylinter
│       └── main.go
├── go.mod
├── mylinter.go
├── mylinter_test.go
└── testdata
    └── src
        └── a
            ├── a.go
            └── go.mod	    

ディレクトリ移動します。

cd mylinter

必要なモジュールをインストールします。

go mod tidy

基本的には、mylinter_test.goを編集します。
このままテストを実行して見ます。落ちることが確認できます。

myanalyzer/myanalyzer_test.go
 $ go test -run TestAnalyzer                                                                    [~/gitrepo/tmp-go/myanalyzer]
--- FAIL: TestAnalyzer (0.03s)
    analysistest.go:456: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern`
    analysistest.go:520: a/a.go:5: no diagnostic was reported matching `pattern`
FAIL
exit status 1
FAIL	myanalyzer	0.149s

テストデータを修正することで、テストを PASS することができます。

myanalyzer/testdata/src/a/a.go
package a

func f() {
	// The pattern can be written in regular expression.
	var gopher int // want "pattern"
	print(gopher)  // want "identifier is gopher"
}

以下のように、want を gopher に編集してみます。

myanalyzer/testdata/src/a/a.go
package a

func f() {
	// The pattern can be written in regular expression.
	var gopher int // want "gopher"
	print(gopher)  // want "identifier is gopher"
}

修正後にテストを回すと、PASS していることがわかります!

myanalyzer/myanalyzer_test.go
 $ go test -run TestAnalyzer                                                                    [~/gitrepo/tmp-go/myanalyzer]
PASS
ok  	myanalyzer	0.150s

ここまでの話は、以下のページに詳細が載っています。
https://github.com/gostaticanalysis/skeleton/blob/main/README_ja.md

静的解析してみる

今回作成する linter

今回作成するのは、「複雑な関数の検出」です。
複雑な関数は色々捉え方がありますが、今回は、「引数が 5 個以上ある関数」ということにします。

テストデータを編集

まず、テストデータを編集します。
今回は引数の数が多いか少ないかを確かめるために、引数の数が異なる関数を 3 つ用意しました。

myanalyzer/testdata/src/a/a.go
package a

// 最初からある関数
func f() {
	// The pattern can be written in regular expression.
	var gopher int // want "g*."
	print(gopher)  // want "identifier is gopher"
}

// 引数がない関数
func f0() {}

// 引数が多く、複雑な関数
func f1(a, b, c, d, e, f int) int {
	return a + b + c + d + e + f
}

// 引数が少なく、簡単な関数
func f2(a int) int {
	return a
}

解析する関数を編集

いよいよ本命の解析する関数をいじります。
初期状態の mysnalyzer.go は以下のようになっています。

myanalyzer/myanalyzer.go
func run(pass *analysis.Pass) (any, 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
}

いじるにあたって、どこの node になんの情報が入っているかわからなくなる時があります。
そこで、以下の抽象構文木を可視化してくれるサイトを使うと便利です。

https://yuroyoro.github.io/goast-viewer/

run 関数を、以下のように編集します。

  • inspect.Preorder の第一引数を nil にする。
  • switch の中で、FuncDecl の場合を追加する。
myanalyzer/myanalyzer.go
func run(pass *analysis.Pass) (any, error) {
	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	inspect.Preorder(nil, func(n ast.Node) {
		switch n := n.(type) {
		case *ast.FuncDecl:
			fmt.Println(n.Name)
		}
	})

	return nil, nil
}

実行すると、以下のような出力になります。
関数の名前が取れていることがわかります!!

=== RUN   TestAnalyzer
f
f0
f1
f2
    analysistest.go:520: a/a.go:5: no diagnostic was reported matching `g*.`
    analysistest.go:520: a/a.go:6: no diagnostic was reported matching `identifier is gopher`
--- FAIL: TestAnalyzer (0.05s)

FuncDecl とは?

FuncDecl は、宣言を表すノードの中の一つで、関数宣言の情報が入っています。
実装をみてみると、関数名の他にも以下のような情報があります。

src/go/ast/ast.go
// A FuncDecl node represents a function declaration.
FuncDecl struct {
	Doc  *CommentGroup // associated documentation; or nil
	Recv *FieldList    // receiver (methods); or nil (functions)
	Name *Ident        // function/method name
	Type *FuncType     // function signature: type and value parameters, results, and position of "func" keyword
	Body *BlockStmt    // function body; or nil for external (non-Go) function
}

FuncDecl の中身を出力させてみます。

myanalyzer/myanalyzer.go
func run(pass *analysis.Pass) (any, error) {
	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	inspect.Preorder(nil, func(n ast.Node) {
		switch n := n.(type) {
		case *ast.FuncDecl:
			fmt.Printf("n.Name: %s \n", n.Name)
			fmt.Printf("n.Doc.List[0].Text: %+v \n", n.Doc.List[0].Text)
			if len(n.Type.Params.List) > 0 {
				fmt.Printf("n.Type.Params.List[0].Names: %+v \n", n.Type.Params.List[0].Names)
			}
			fmt.Println()
		}
	})

	return nil, nil
}

FuncDecl は、関数の引数やドキュメントの情報が入っていることがわかります。

n.Name: f 
n.Doc.List[0].Text: // 最初からある関数

n.Name: f0 
n.Doc.List[0].Text: // 引数がない関数 

n.Name: f1 
n.Doc.List[0].Text: // 引数が多く、複雑な関数 
n.Type.Params.List[0].Names: [a b c d e f] 

n.Name: f2 
n.Doc.List[0].Text: // 引数が少なく、簡単な関数 
n.Type.Params.List[0].Names: [a] 

以上を踏まえて、引数の数を調べる

引数の情報は、n.Type.Params.List に入っています。
なので、このリストの個数を調べれば、引数の数がわかりそうです!

myanalyzer/myanalyzer.go
func run(pass *analysis.Pass) (any, error) {
	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	inspect.Preorder(nil, func(n ast.Node) {
		switch n := n.(type) {
		case *ast.FuncDecl:
			// 引数があるかどうかチェック
			if len(n.Type.Params.List) != 0 {
				// 引数の数をチェック
				params := len(n.Type.Params.List[0].Names)
				if params > 5 {
					// 引数が5以上だったら警告を出す
					pass.Reportf(n.Pos(), "too many arguments: func name: %s", n.Name.Name)
				}
			}
		}
	})

	return nil, nil
}

また、テストデータも編集する必要があります。

myanalyzer/testdata/src/a/a.go
package a

// 最初からある関数
func f() {
	// The pattern can be written in regular expression.
	var gopher int
	print(gopher)
}

// 引数がない関数
func f0() {}

// 引数が多く、複雑な関数
func f1(a, b, c, d, e, f int) int { // want "too many arguments: func name: f1"
	return a + b + c + d + e + f
}

// 引数が少なく、簡単な関数
func f2(a int) int {
	return a
}

テストを回すと通っていることが確認できます!

=== RUN   TestAnalyzer
--- PASS: TestAnalyzer (0.04s)
PASS

おわりに

今回は、関数の引数の個数を調べるだけの linter を作成しました。
静的解析には興味があったのですが、なにからやれば良いかわからなかったのですが、
今回の tenntenn さんの講義を受けて、基礎は身についたかと思います。
この調子で、難易度の高い linter も作成したいです!!

Discussion