gofmt は式に含まれるスペースをいい感じに調整してくれる

夏にこんなツイートをしたんです。

<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr">gofmt 、 + 演算子の両隣にホワイトスペースが出るときと出ないときがある。 &quot;abc&quot; + &quot;def&quot; と &quot;abc&quot;+&quot;def&quot; が混ざって気持ち悪い。</p>&mdash; Taichi Sasaki (@tchssk) <a href="https://twitter.com/tchssk/status/630943134373916672">2015, 8月 11</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

そうしたら mattn さんの目に留まったらしく、 [github.com/golang/go](https://github.com/golang/go) で質問してくれました。

[cmd/gofmt: doesn't unity spaces in expression with plus operator #12105](https://github.com/golang/go/issues/12105)

>When two arguments are given, gofmt removes spaces both sides in plus operator. Is this expected?

すぐに Rob Pike からの返事。

>Yes. Gofmt uses spaces to show grouping appropriate to operator precedence.

なるほど。 Go は演算子の優先度を考慮してスペースを調整してくれるようです。

不具合ではないということでその時はスルーしてしまったんですが、今回あらためて gofmt ([cmd/gofmt](https://github.com/golang/go/tree/master/src/cmd/gofmt)) のコードを読んでみました。上記 Issue に mattn さんが貼ったサンプルコードで、どのようにスペースが調整されてるかを追っていきます。

```go:sample.go
package main

import "fmt"

func main() {
	s := "foo" + "bar"                    // space plus space
	fmt.Println(s)                        //
	fmt.Println("foo" + "bar")            // space plus space
	fmt.Println("foo"+"bar", "bar"+"baz") // just plus
}
```

## gofmt を読む

まず [gofmtMain()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/cmd/gofmt/gofmt.go#L165-L209) で引数を判定して [processFile()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/cmd/gofmt/gofmt.go#L76-L141) を呼び出しています。

processFile の要点は 2 つ。

1. 入力されたコードの AST (抽象構文木) を取得 ([internal/format.Parse()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/internal/format/format.go#L20-L86))
2. 取得した AST をフォーマット ([internal/format.Format()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/internal/format/format.go#L91-L157))

スペース数の調整は当然 2 で行われています。

### 1. 入力されたコードの AST (抽象構文木) を取得

[internal/format.Parse()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/internal/format/format.go#L20-L86) から [go/parser.ParseFile()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/parser/interface.go#L84-L120) を呼び出して AST を取得しています。取得した AST を可視化すると以下のようになります。 (可視化には [GoAst Viewer](http://goast.yuroyoro.net/) を使わせていただきました)

![ast.png](https://qiita-image-store.s3.amazonaws.com/0/83093/f286b7f6-9055-bbc8-ad09-a169bf1758b2.png)

これは元のコードのうち以下の部分を表しています。

```go:sample.go
	fmt.Println("foo" + "bar")            // space plus space
	fmt.Println("foo"+"bar", "bar"+"baz") // just plus
```



### 2. 取得した AST をフォーマット

[internal/format.Format()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/internal/format/format.go#L91-L157) から [go/printer.Config.Fprint()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/printer.go#L1287-L1289) →  [go/printer.Config.fprint()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/printer.go#L1226-L1272) → [go/printer.printNode()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/printer.go#L1017-L1100) とたどっていきます。

このメソッドで AST の node に応じた分岐をしています。今回挙動を知りたいのは ast.Expr です。さらにたどる ... 。 [go/printer.expr()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/nodes.go#L898-L901) → [go/printer.expr1()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/nodes.go#L678-L892)

ここでまた分岐です。今回は ast.CallExpr 。

```go:nodes.go
	case *ast.CallExpr:
		if len(x.Args) > 1 {
			depth++
		}
		if _, ok := x.Fun.(*ast.FuncType); ok {
			// conversions to literal function types require parentheses around the type
			p.print(token.LPAREN)
			p.expr1(x.Fun, token.HighestPrec, depth)
			p.print(token.RPAREN)
		} else {
			p.expr1(x.Fun, token.HighestPrec, depth)
		}
		p.print(x.Lparen, token.LPAREN)
		if x.Ellipsis.IsValid() {
			p.exprList(x.Lparen, x.Args, depth, 0, x.Ellipsis)
			p.print(x.Ellipsis, token.ELLIPSIS)
			if x.Rparen.IsValid() && p.lineFor(x.Ellipsis) < p.lineFor(x.Rparen) {
				p.print(token.COMMA, formfeed)
			}
		} else {
			p.exprList(x.Lparen, x.Args, depth, commaTerm, x.Rparen)
		}
		p.print(x.Rparen, token.RPAREN)
```

関数名の部分 (x.Fun) は [go/printer.expr1()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/nodes.go#L678-L892) を再帰的に呼び出していますね。。末端ノードまでの再起呼び出しで AST を処理しているわけです。

### 引数が 2 つ以上の場合に入る `, `

そして引数部分 (x.Args) 。こちらは [go/printer.exprList()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/nodes.go#L124-L269) を呼んでいます。

```go
		for i, x := range list {
			if i > 0 {
				// use position of expression following the comma as
				// comma position for correct comment placement
				p.print(x.Pos(), token.COMMA, blank)
			}
			p.expr0(x, depth)
		}
```

引数が 2 つ以上あった場合、 `, ` を挿入しています。
`fmt.Println("foo"+"bar", "bar"+"baz")` の `, ` を出力しているのがここです。

やっとスペースを出しているところにたどり着いた ...!

しかしまだ終わっていません。 `+` の両端のスペースが残っています。

### `+` の両端に入る (ときと入らないときがある) スペース

各引数は [go/printer.expr0()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/nodes.go#L894-L896) → [go/printer.expr1()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/printer/nodes.go#L678-L892) とまた再帰呼び出し。今度は ast.BinaryExpr です。

```go
func (p *printer) binaryExpr(x *ast.BinaryExpr, prec1, cutoff, depth int) {
	prec := x.Op.Precedence()
	if prec < prec1 {
		// parenthesis needed
		// Note: The parser inserts an ast.ParenExpr node; thus this case
		//       can only occur if the AST is created in a different way.
		p.print(token.LPAREN)
		p.expr0(x, reduceDepth(depth)) // parentheses undo one level of depth
		p.print(token.RPAREN)
		return
	}

	printBlank := prec < cutoff

	ws := indent
	p.expr1(x.X, prec, depth+diffPrec(x.X, prec))
	if printBlank {
		p.print(blank)
	}
	xline := p.pos.Line // before the operator (it may be on the next line!)
	yline := p.lineFor(x.Y.Pos())
	p.print(x.OpPos, x.Op)
	if xline != yline && xline > 0 && yline > 0 {
		// at least one line break, but respect an extra empty line
		// in the source
		if p.linebreak(yline, 1, ws, true) {
			ws = ignore
			printBlank = false // no blank after line break
		}
	}
	if printBlank {
		p.print(blank)
	}
	p.expr1(x.Y, prec+1, depth+1)
	if ws == ignore {
		p.print(unindent)
	}
}
```

`printBlank` が `true` だったらスペースを入れています。判定に使っている `prec` には [go/token.Precedence()](https://github.com/golang/go/blob/40cbf58f960a8f5287d2c3a93b3ca6119df67e85/src/go/token/token.go#L259-L273) からオペレータの優先度が入ります。 `+` なら 4 。 `cutoff` には
メソッド呼び出し時に `cutoff()` で判定した優先度が入ってきます。

`+` の前後にスペースを出すかはここで制御しています。

長かった ...!

## あとがき

作業の合間に少しずつ読み進めていたら、最後にたどり着くまで数日かかってしまいました。

mattn さんはというと

<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/tchssk">@tchssk</a> go/printer/nodes.go で CallExpr 見てるとこがそれですね。</p>&mdash; mattn (@mattn_jp) <a href="https://twitter.com/mattn_jp/status/630953480484618240">2015, 8月 11</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

一瞬で目的のコードまでたどり着いてました。さすがです。
標準パッケージもっと読んで精進します。