Goの静的解析ツールを作ったので紹介したいと思います。
作ったもの
何ができるのか
- タイトルにもある通り、Goの静的解析をしてくれます
- Brace( "{" や "}" )の前後にある空白行を検知することができます
次のコードを例に取ります。
突っ込み所はとてもありますが、一番気になる(?)のは、レビューで指摘されそうな、12行目や20行目に存在する空白行ではないでしょうか。
1 package main
2
3 import "fmt"
4
5 type cat struct {
6 name string
7 age int
8 }
9
10 func newCat(name string, age int) cat {
11 return cat{
12
13 name: name,
14 age: age,
15 }
16 }
17
18 func foo(a, b int) int {
19 if a < b {
20
21 a = a + b
22
23 }
24
25 return 2
26 }
27
28 func main() {
29
30 fmt.Println("test comment")
31
32 }
このコードに対して、
$ go vet -vettool=$GOPATH/bin/blahbrah $PKG_NAME
を実行します。
すると、次のような出力が返ってきます。
$ go vet -vettool=$GOPATH/bin/blahbrah $PKG_NAME
./main.go:12:1: ineffectual blank line after the left brace
./main.go:20:1: ineffectual blank line after the left brace
./main.go:22:1: ineffectual blank line before the right brace
./main.go:29:1: ineffectual blank line after the left brace
./main.go:31:1: ineffectual blank line before the right brace
謎の空白がある行が指定されていますね!
といったことをしてくれるツールを作成しました。
インストール方法
$ go install github.com/granddaifuku/blahbrah/cmd/blahbrah@latest
内部について
使ったツール
- skeleton:静的解析用の雛形を作ってくれるCLIツールです。
方針
前提として、gofmt
やgoimports
なりでコードが整形された後に使用することを想定しています。
- 各ファイルを走査
- ファイル内のコメントがある行を記録
- ファイル内の
Block Statement
とComposite Literal
を発見したタイミングで下記チェックを行う - チェックによって得られたレポートからDiagnosticを生成
といった流れで静的解析を行います。
チェック内容
A. 左Brace
左Braceに関して:Brace直後のコードがある行を探し、その行とBraceとの行間が1以上あってかつ、行間がコメントでなければ、無益な空行であるとみなしてレポートを作成します。
B. 右Brace
右Braceに関して:左Braceと同様の処理を直前のコードがある行に対して行います。
これらをソースコード例とともに示すと次のようになります。
1 package main
2
3 import "fmt"
4
5 func main() {
6 // This is a successful comment
7 fmt.Println("test comment")
8 }
上記ok.go
内では左Brace(5行目)と直後のコード(7行目)には1行以上の差がありますが、中間である6行目にはコメントがあるため、blahbrah
はDiagnosticを生成しません。
1 package main
2
3 import "fmt"
4
5 func main() {
6
7 fmt.Println("test comment")
8 }
それに対して、上記ng.go
では6行目はただの空行となっているため、次のようにDiagnosticを生成します。
$ go vet -vettool=$GOPATH/bin/blahbrah $PKG_NAME
./main.go:6:1: ineffectual blank line after the left brace
これらがblahbrah
による静的解析の大まかな流れです。
実装
1 func run(pass *analysis.Pass) (interface{}, error) {
2 for _, f := range pass.Files {
3 c := newChecker(pass.Fset, f.Decls, f.Comments)
4 reports := c.inspect()
5 for _, r := range reports {
6 pass.Reportf(token.Pos(r.pos), r.msg)
7 }
8 }
9
10 return nil, nil
11 }
上記のrun
関数内2行目にて、各ファイルを走査しています。
3~4行目ではchecker
と呼ばれる構造体を生成し、その中でレポート(静的解析によって検知した位置やその内容)を作成、返却しています。
最後に、5~7行目にてDiagnosticを生成しています。
では、続いてchecker
構造体について見ていきましょう。
1 type checker struct {
2 fset *token.FileSet
3 decls []ast.Decl
4 comments map[int]struct{}
5 }
6
7 func newChecker(
8 fset *token.FileSet,
9 decls []ast.Decl,
10 cg []*ast.CommentGroup,
11 ) *checker {
12 // create a map whose key is the line number of comments
13 comments := make(map[int]struct{})
14
15 for _, c := range cg {
16 if c.Text() == testAfterLbrace || c.Text() == testBeforeRbrace {
17 continue
18 }
19 start := fset.Position(c.Pos()).Line
20 end := fset.Position(c.End()).Line
21 for l := start; l < end+1; l++ {
22 comments[l] = struct{}{}
23 }
24 }
25
26 return &checker{
27 fset: fset,
28 decls: decls,
29 comments: comments,
30 }
31 }
checker
構造体はフィールドにソースコードの位置に関する情報をもつfset
、パッケージ内での宣言群であるdecls
、コメントのある行を把握するためのcomments
を持っています。
7~31行目にかけて、newChecker()
関数ではchecker
を生成します。
15~24行目ではコメント情報を受け取り、コメントがある行数をmapへと保存しています。
続いて、レポートを作成するinspect()
メソッドの紹介です。
1 func (c *checker) inspect() []report {
2 reports := make([]report, 0)
3
4 for _, d := range c.decls {
5 switch d := d.(type) {
6 case *ast.FuncDecl:
7 b := d.Body
8 ast.Inspect(b, func(n ast.Node) bool {
9 switch n := n.(type) {
10 case *ast.BlockStmt:
11 r := c.blockStmt(n)
12 if r != nil {
13 reports = append(reports, r...)
14 }
15 case *ast.CompositeLit:
16 r := c.compositeLit(n)
17 if r != nil {
18 reports = append(reports, r...)
19 }
20 }
21
22 return true
23 })
24 }
25 }
26
27 return reports
28 }
inspect()
メソッドでは各宣言に対して、DFSによって生成されたASTを辿る、ast.inspect()
メソッドを呼び出しています。
その中で、Block Statement
とComposite Literal
を発見した際に上記、チェック内容で説明した処理を行うメソッドを呼び出しています。
Block Statement
のパターンを例に取り、実装を見ていきます。
1 func (c *checker) blockStmt(
2 block *ast.BlockStmt,
3 ) []report {
4 lbraceLine := c.line(block.Lbrace)
5 rbraceLine := c.line(block.Rbrace)
6
7 if len(block.List) == 0 {
8 if rbraceLine-lbraceLine > 1 && !c.isComment(lbraceLine+1) {
9 return []report{
10 {
11 pos: int(block.Rbrace) - c.col(block.Rbrace),
12 msg: beforeRbrace,
13 },
14 }
15 }
16
17 return nil
18 }
19
20 reports := make([]report, 0)
21
22 firstLine := c.line(block.List[0].Pos())
23 firstCol := c.col(block.List[0].Pos())
24 if firstLine-lbraceLine > 1 && !c.isComment(lbraceLine+1) {
25 r := report{
26 pos: int(block.List[0].Pos()) - firstCol,
27 msg: afterLbrace,
28 }
29 reports = append(reports, r)
30 }
31
32 endLine := c.line(block.List[len(block.List)-1].End())
33 if rbraceLine-endLine > 1 && !c.isComment(rbraceLine-1) {
34 r := report{
35 pos: int(block.Rbrace) - c.col(block.Rbrace),
36 msg: beforeRbrace,
37 }
38 reports = append(reports, r)
39 }
40
41 return reports
42 }
blockStmt()
メソッドはレポートのスライスを返します。
7~18行目では、左右Brace間にソースコードがなかった場合を考えています。
この時、Braceの行間が1以上かつ、その行がコメントではない場合、レポートを生成します。
22~30行目では左Braceに対してチェック内容Aを実施しています。
同様に32~39行目では右Braceに対してチェック内容Bを実施しています。
終わり
以上、Goの静的解析ツールblahbrah
の紹介でした。
使っていただけたら嬉しいです。
また、改善や提案などもありがたく受け付けております。