[インデックス 13077] ファイルの概要
このコミットは、Go言語の公式ツールであるgo/printer
パッケージにおける挙動の修正に関するものです。具体的には、空のステートメント(;
のみの記述)がソースコード中に存在する場合に、go/printer
が余分な改行を出力しないように変更されています。これにより、gofmt
などのフォーマッタがより整形されたコードを生成できるようになります。
コミット
commit 03e1d4bc2262de4d50e0fe9ebcf743f1f1aef479
Author: Robert Griesemer <gri@golang.org>
Date: Tue May 15 12:21:21 2012 -0700
go/printer: don't print newlines for empty statements
Fixes #3466.
gofmt -w src misc causes no changes.
R=rsc
CC=golang-dev
https://golang.org/cl/6206073
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/03e1d4bc2262de4d50e0fe9ebcf743f1f1aef479
元コミット内容
このコミットの目的は、go/printer
が空のステートメントに対して改行を出力しないようにすることです。これにより、gofmt
がソースコードを整形する際に、不要な改行が挿入される問題(Issue #3466)が修正されます。コミットメッセージには、「gofmt -w src misc causes no changes.
」とあり、この変更がgofmt
の出力に悪影響を与えないことを示唆しています。
変更の背景
この変更は、Go言語のIssue #3466「gofmt makes many new lines from many semicolons」を修正するために行われました。このIssueでは、Goのソースコード中に複数のセミコロン(;
)が連続して記述された場合、gofmt
がそれらを整形する際に、各セミコロンの後に余分な改行を挿入してしまうという問題が報告されていました。
Go言語では、ステートメントの終端にセミコロンを記述しますが、改行がある場合はセミコロンを省略できます。しかし、意図的に複数の空のステートメントを記述したり、コード生成などによって連続したセミコロンが挿入されたりするケースも考えられます。このような場合にgofmt
が過剰な改行を挿入すると、コードの可読性が損なわれたり、不必要な差分が生じたりする問題がありました。
このコミットは、go/printer
がAST(抽象構文木)を整形して出力する際に、ast.EmptyStmt
(空のステートメントを表すASTノード)を検出した場合に、その後の改行の挿入を抑制することで、この問題を解決しようとしています。
前提知識の解説
Go言語のgo/printer
パッケージ
go/printer
パッケージは、Go言語の標準ライブラリの一部であり、Goのソースコードを「整形して出力(pretty-print)」するための機能を提供します。具体的には、go/ast
パッケージで表現される抽象構文木(AST)を受け取り、それを人間が読みやすい形式のGoソースコードに変換します。gofmt
ツールは、このgo/printer
パッケージを利用してGoのコードを自動整形しています。
go/printer
は、単にASTを文字列に変換するだけでなく、Goの公式なスタイルガイドに沿ってインデント、改行、スペースなどを適切に配置する役割を担っています。これにより、Goのコードベース全体で一貫したフォーマットが保たれるようになっています。
Go言語のast.EmptyStmt
Go言語のコンパイラやツールは、ソースコードを解析して抽象構文木(AST)を構築します。このASTは、プログラムの構造を木構造で表現したものです。go/ast
パッケージは、このASTの各ノードを定義しています。
ast.EmptyStmt
は、GoのASTにおいて「空のステートメント」を表す構造体です。Go言語では、セミコロン(;
)のみで構成されるステートメントは有効な空のステートメントとして扱われます。例えば、for {}
ループの本体で何も処理を行わない場合に;
を記述したり、ラベルの後に;
を記述したりするケースが考えられます。
ast.EmptyStmt
の定義は以下のようになっています。
type EmptyStmt struct {
Semicolon token.Pos // position of the semicolon
Implicit bool // true if semicolon was omitted in source
}
Semicolon
: 空のステートメントを構成するセミコロンの位置情報(ソースコード上の行と列)を保持します。Implicit
: セミコロンがソースコード中で明示的に記述されず、Goの文法規則によって暗黙的に挿入された場合にtrue
となります(例: 行末の改行によるセミコロンの自動挿入)。
このコミットでは、go/printer
がASTを走査する際にast.EmptyStmt
ノードを識別し、そのノードに対して特別な処理を適用することで、不要な改行の出力を抑制しています。
技術的詳細
このコミットの技術的な核心は、go/printer
パッケージ内のstmtList
関数における変更です。stmtList
関数は、ステートメントのリスト([]ast.Stmt
)を整形して出力する役割を担っています。
変更前は、stmtList
関数は単純にステートメントのリストをループし、各ステートメントの前に改行を挿入していました。この際、ast.EmptyStmt
であっても、他の通常のステートメントと同様に扱われ、その結果として余分な改行が生成されていました。
変更後では、stmtList
関数内でステートメントを処理するループの前に、以下のチェックが追加されました。
// ignore empty statements (was issue 3466)
if _, isEmpty := s.(*ast.EmptyStmt); !isEmpty {
// ... 既存のステートメント処理ロジック ...
}
このif
文は、現在のステートメントs
がast.EmptyStmt
型であるかどうかをチェックしています。
s.(*ast.EmptyStmt)
: 型アサーションを行い、s
が*ast.EmptyStmt
型に変換可能であれば、その値とtrue
を返します。変換不可能であれば、ゼロ値とfalse
を返します。!isEmpty
:isEmpty
がfalse
の場合(つまり、現在のステートメントがast.EmptyStmt
ではない場合)にのみ、内部のステートメント処理ロジックが実行されます。
これにより、stmtList
関数はast.EmptyStmt
を検出した場合、そのステートメントを「無視」し、それに対する改行の挿入やその他の整形処理を行わなくなります。結果として、連続する空のステートメントや、他のステートメントと空のステートメントが混在する場合でも、go/printer
は余分な改行を出力せず、よりコンパクトで意図通りの整形結果を生成できるようになります。
また、ループカウンタi
の扱いも変更されています。変更前はfor i, s := range list
で直接i
を使用していましたが、変更後はast.EmptyStmt
をスキップするため、i
を明示的にインクリメントするようになりました。これにより、linebreak
関数に渡されるi
が、実際に整形されるステートメントのインデックスを正確に反映するようになります。
コアとなるコードの変更箇所
src/pkg/go/printer/nodes.go
--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -868,28 +868,32 @@ func (p *printer) expr(x ast.Expr) {
// Print the statement list indented, but without a newline after the last statement.
// Extra line breaks between statements in the source are respected but at most one
// empty line is printed between statements.
-func (p *printer) stmtList(list []ast.Stmt, _indent int, nextIsRBrace bool) {
- // TODO(gri): fix _indent code
- if _indent > 0 {
+func (p *printer) stmtList(list []ast.Stmt, nindent int, nextIsRBrace bool) {
+ if nindent > 0 {
p.print(indent)
}
multiLine := false
- for i, s := range list {
- // _indent == 0 only for lists of switch/select case clauses;
- // in those cases each clause is a new section
- p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || _indent == 0 || multiLine)
- p.stmt(s, nextIsRBrace && i == len(list)-1)
- multiLine = p.isMultiLine(s)
- }
- if _indent > 0 {
+ i := 0
+ for _, s := range list {
+ // ignore empty statements (was issue 3466)
+ if _, isEmpty := s.(*ast.EmptyStmt); !isEmpty {
+ // _indent == 0 only for lists of switch/select case clauses;
+ // in those cases each clause is a new section
+ p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || nindent == 0 || multiLine)
+ p.stmt(s, nextIsRBrace && i == len(list)-1)
+ multiLine = p.isMultiLine(s)
+ i++
+ }
+ }
+ if nindent > 0 {
p.print(unindent)
}
}
// block prints an *ast.BlockStmt; it always spans at least two lines.
-func (p *printer) block(s *ast.BlockStmt, indent int) {\n+\tfunc (p *printer) block(s *ast.BlockStmt, nindent int) {\n \tp.print(s.Pos(), token.LBRACE)\n-\tp.stmtList(s.List, indent, true)\n+\tp.stmtList(s.List, nindent, true)\n \tp.linebreak(p.lineFor(s.Rbrace), 1, ignore, true)\n \tp.print(s.Rbrace, token.RBRACE)\n }\n```
### `src/pkg/go/printer/testdata/statements.golden` および `src/pkg/go/printer/testdata/statements.input`
これらのファイルは、`go/printer`のテストデータです。
* `statements.input`: テストの入力となるGoのソースコード。
* `statements.golden`: `go/printer`によって整形された後の期待される出力(ゴールデンファイル)。
このコミットでは、空のステートメントのフォーマットに関する新しいテストケースが追加されています。これにより、変更が正しく機能し、空のステートメントが適切に処理されることが保証されます。
## コアとなるコードの解説
このコミットの主要な変更は、`src/pkg/go/printer/nodes.go`ファイル内の`stmtList`関数にあります。
1. **引数名の変更**: `_indent`から`nindent`に引数名が変更されています。これは機能的な変更ではなく、命名規則の改善です。
2. **ループの変更と空ステートメントの無視**:
* 変更前: `for i, s := range list`
* Goの`range`キーワードは、スライスやマップの要素をイテレートする際に、インデックスと値の両方を返します。この場合、`i`はリスト内のステートメントのインデックスでした。
* 変更後: `i := 0` と `for _, s := range list`
* `i`がループの外で初期化され、ループ内で明示的にインクリメントされるようになりました。
* `for _, s := range list`は、インデックスを無視して値のみをイテレートします。
* **`if _, isEmpty := s.(*ast.EmptyStmt); !isEmpty { ... }`**: この行が最も重要な変更点です。
* `s.(*ast.EmptyStmt)`は、現在のステートメント`s`が`ast.EmptyStmt`型であるかどうかをチェックする型アサーションです。
* `isEmpty`が`true`の場合(つまり、`s`が空のステートメントである場合)、`!isEmpty`は`false`となり、`if`ブロック内のコードは実行されません。これにより、空のステートメントは整形処理から除外されます。
* `isEmpty`が`false`の場合(つまり、`s`が空のステートメントではない場合)、`!isEmpty`は`true`となり、`if`ブロック内のコードが実行され、通常のステートメントとして整形されます。
* `i++`は、実際に整形されるステートメントの数をカウントするために、`if`ブロックの内部に移動しました。これにより、`linebreak`関数に渡される`i`が、空のステートメントをスキップした後の正しいインデックスを示すようになります。
この変更により、`go/printer`は空のステートメントを検出した際に、それに対応する改行を出力しないようになります。これは、`gofmt`がGoのコードを整形する際の挙動に直接影響を与え、Issue #3466で報告された問題(連続するセミコロンによる過剰な改行)を解決します。
## 関連リンク
* Go Issue #3466: [https://github.com/golang/go/issues/3466](https://github.com/golang/go/issues/3466)
* Go CL 6206073: [https://golang.org/cl/6206073](https://golang.org/cl/6206073) (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
## 参考にした情報源リンク
* Go言語 `go/ast` パッケージドキュメント: [https://pkg.go.dev/go/ast](https://pkg.go.dev/go/ast)
* Go言語 `go/printer` パッケージドキュメント: [https://pkg.go.dev/go/printer](https://pkg.go.dev/go/printer)
* Go言語 `ast.EmptyStmt` の解説 (Web検索結果より): [https://go.dev/src/go/ast/ast.go](https://go.dev/src/go/ast/ast.go) (Goのソースコード内の`ast.go`ファイル)
* Go言語 `go/printer` の解説 (Web検索結果より): [https://go.dev/src/go/printer/printer.go](https://go.dev/src/go/printer/printer.go) (Goのソースコード内の`printer.go`ファイル)
* Go Issue #3466に関するGitHubの議論 (Web検索結果より): [https://github.com/golang/go/issues/3466](https://github.com/golang/go/issues/3466)