Goの静的解析ツールblahbrah

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

## 作ったもの
- [blahbrah](https://github.com/granddaifuku/blahbrah)

### 何ができるのか
- タイトルにもある通り、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	}
```
このコードに対して、
```sh
$ go vet -vettool=$GOPATH/bin/blahbrah $PKG_NAME
```
を実行します。
すると、次のような出力が返ってきます。
```sh
$ 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
```
謎の空白がある行が指定されていますね！

といったことをしてくれるツールを作成しました。

### インストール方法
```sh
$ go install github.com/granddaifuku/blahbrah/cmd/blahbrah@latest
```

## 内部について
### 使ったツール
- [skeleton](https://github.com/gostaticanalysis/skeleton)：静的解析用の雛形を作ってくれるCLIツールです。

### 方針
前提として、`gofmt`や`goimports`なりでコードが整形された後に使用することを想定しています。
1. 各ファイルを走査
2. ファイル内のコメントがある行を記録
3. ファイル内の`Block Statement`と`Composite 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を生成します。
```shell
$ 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 Statement`と`Composite 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](#a-左brace)を実施しています。
同様に32~39行目では右Braceに対して[チェック内容B](#b-右brace)を実施しています。

## 終わり
以上、Goの静的解析ツール`blahbrah`の紹介でした。
使っていただけたら嬉しいです。
また、改善や提案などもありがたく受け付けております。

## 参考
- [プログラミング言語Go完全入門 14. 静的解析とコード生成](https://docs.google.com/presentation/d/1I4pHnzV2dFOMbRcpA-XD0TaLcX6PBKpls6WxGHoMjOg/edit#slide=id.g80ffbfd5e3_0_168)