LoginSignup
3
0

More than 1 year has passed since last update.

Goの静的解析ツールblahbrah

Last updated at Posted at 2022-03-03

Goの静的解析ツールを作ったので紹介したいと思います。

作ったもの

何ができるのか

  • タイトルにもある通り、Goの静的解析をしてくれます
  • Brace( "{" や "}" )の前後にある空白行を検知することができます

次のコードを例に取ります。
突っ込み所はとてもありますが、一番気になる(?)のは、レビューで指摘されそうな、12行目や20行目に存在する空白行ではないでしょうか。

main.go
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ツールです。

方針

前提として、gofmtgoimportsなりでコードが整形された後に使用することを想定しています。

  1. 各ファイルを走査
  2. ファイル内のコメントがある行を記録
  3. ファイル内のBlock StatementComposite Literalを発見したタイミングで下記チェックを行う
  4. チェックによって得られたレポートからDiagnosticを生成

といった流れで静的解析を行います。

チェック内容

A. 左Brace

左Braceに関して:Brace直後のコードがある行を探し、その行とBraceとの行間が1以上あってかつ、行間がコメントでなければ、無益な空行であるとみなしてレポートを作成します。

B. 右Brace

右Braceに関して:左Braceと同様の処理を直前のコードがある行に対して行います。
これらをソースコード例とともに示すと次のようになります。

ok.go
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を生成しません。

ng.go
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による静的解析の大まかな流れです。

実装

blahbrah.go
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構造体について見ていきましょう。

checker.go
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()メソッドの紹介です。

checker.go
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 StatementComposite Literalを発見した際に上記、チェック内容で説明した処理を行うメソッドを呼び出しています。

Block Statementのパターンを例に取り、実装を見ていきます。

checker.go
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の紹介でした。
使っていただけたら嬉しいです。
また、改善や提案などもありがたく受け付けております。

参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0