Go言語で静的解析をやってみる
はじめに
Gopher塾 #3 に参加し、静的解析について学びました。
やったことを忘れないため & アウトプットのためにまとめたいと思います。
実際に行った流れの通りに説明しようと思います。
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
を編集します。
このままテストを実行して見ます。落ちることが確認できます。
$ 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 することができます。
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 に編集してみます。
package a
func f() {
// The pattern can be written in regular expression.
var gopher int // want "gopher"
print(gopher) // want "identifier is gopher"
}
修正後にテストを回すと、PASS していることがわかります!
$ go test -run TestAnalyzer [~/gitrepo/tmp-go/myanalyzer]
PASS
ok myanalyzer 0.150s
ここまでの話は、以下のページに詳細が載っています。
静的解析してみる
今回作成する linter
今回作成するのは、「複雑な関数の検出」です。
複雑な関数は色々捉え方がありますが、今回は、「引数が 5 個以上ある関数」ということにします。
テストデータを編集
まず、テストデータを編集します。
今回は引数の数が多いか少ないかを確かめるために、引数の数が異なる関数を 3 つ用意しました。
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 は以下のようになっています。
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 になんの情報が入っているかわからなくなる時があります。
そこで、以下の抽象構文木を可視化してくれるサイトを使うと便利です。
run 関数を、以下のように編集します。
- inspect.Preorder の第一引数を nil にする。
- switch の中で、FuncDecl の場合を追加する。
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 は、宣言を表すノードの中の一つで、関数宣言の情報が入っています。
実装をみてみると、関数名の他にも以下のような情報があります。
// 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 の中身を出力させてみます。
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
に入っています。
なので、このリストの個数を調べれば、引数の数がわかりそうです!
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
}
また、テストデータも編集する必要があります。
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