Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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文は、現在のステートメントsast.EmptyStmt型であるかどうかをチェックしています。

  • s.(*ast.EmptyStmt): 型アサーションを行い、s*ast.EmptyStmt型に変換可能であれば、その値とtrueを返します。変換不可能であれば、ゼロ値とfalseを返します。
  • !isEmpty: isEmptyfalseの場合(つまり、現在のステートメントが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)