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

[インデックス 18634] ファイルの概要

このコミットは、Go言語の公式フォーマッタである go/printer パッケージにおける、ラベル付きステートメント内のコメントの配置(アライメント)に関するバグを修正するものです。具体的には、ラベル付きステートメントに続くコメントが正しくインデントされず、コードの可読性を損なう問題に対処しています。

コミット

commit c738591e7e9b3ed9804c1a1348ccf7c7c596b8c3
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Feb 24 19:18:16 2014 -0800

    go/printer: fix alignment of comments in labeled statements
    
    Does not change src, misc formatting.
    
    Fixes #5623.
    
    LGTM=r
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/68400043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/c738591e7e9b3ed9804c1a1348ccf7c7c596b8c3

元コミット内容

このコミットは、go/printer パッケージにおいて、ラベル付きステートメント(L: のようなラベルが付いた gotofor などのステートメント)に付随するコメントのインデントが正しく行われない問題を修正します。この修正は、ソースコードのセマンティクスやその他のフォーマットには影響を与えません。

この変更は、Go issue #5623 で報告された問題を解決します。

変更の背景

Go言語には、コードのフォーマットを自動的に行う gofmt というツールがあります。このツールは、Goのソースコードを標準的なスタイルに整形し、コードベース全体の一貫性を保つ上で非常に重要な役割を果たしています。gofmt の内部では、go/printer パッケージが実際にAST(抽象構文木)を走査し、整形されたコードを生成する役割を担っています。

Go issue #5623 は、go/printer がラベル付きステートメントの後に続くコメントを適切にインデントできないというバグを報告しています。具体的には、ラベル付きステートメントの本体が複数行にわたる場合、その本体に付随する行コメントやブロックコメントが、ラベルのインデントレベルではなく、ラベルの開始位置に揃ってしまうという問題がありました。これにより、整形後のコードの可読性が損なわれ、開発者が手動で修正する必要が生じていました。

この問題は、go/printer がステートメントの「複数行性」を判断する際に、ラベル付きステートメント全体を考慮してしまい、ラベル自体が複数行にわたるかのように誤解釈していたことに起因します。本来、コメントのアライメントは、ラベルではなく、そのラベルが指す実際のステートメントのインデントレベルに揃えるべきです。

前提知識の解説

  • Go言語のAST (Abstract Syntax Tree): Goコンパイラやツール(gofmt など)は、Goのソースコードを解析して、その構造を表現する抽象構文木(AST)を生成します。ASTは、プログラムの構造を階層的に表現したもので、各ノードがコードの要素(変数宣言、関数呼び出し、ステートメントなど)に対応します。go/printer はこのASTを受け取り、整形されたコードを出力します。
  • go/printer パッケージ: Go言語の標準ライブラリの一部であり、GoのASTを整形されたGoのソースコードに変換する機能を提供します。gofmt ツールはこのパッケージを利用しています。go/printer は、Goのコードスタイルガイドラインに沿って、インデント、スペース、改行などを自動的に調整します。
  • ラベル付きステートメント (Labeled Statements): Go言語では、for ループや switch ステートメントなどの特定のステートメントにラベルを付けることができます。これは主に breakcontinue ステートメントで特定のループや switch から抜け出す際に使用されます。構文は LabelName: Statement の形式です。 例:
    Loop:
        for i := 0; i < 10; i++ {
            // ...
            if condition {
                break Loop // Loopラベルのforループを抜ける
            }
        }
    
  • コメントのアライメント: プログラミングにおいて、コードの可読性を高めるために、コメントを特定の列に揃える(アライメントする)ことは一般的な慣習です。gofmt のようなフォーマッタは、このアライメントを自動的に維持しようとします。

技術的詳細

この修正は、src/pkg/go/printer/nodes.go ファイル内の stmtList メソッドと、新しく追加された unlabeledStmt ヘルパー関数に焦点を当てています。

stmtList メソッドは、ステートメントのリスト(例: 関数本体のステートメント、iffor のブロック内のステートメント)を整形する役割を担っています。このメソッド内で、各ステートメントが複数行にわたるかどうかを判断するために p.isMultiLine(s) が呼び出されます。

問題は、ラベル付きステートメント s*ast.LabeledStmt 型である場合、p.isMultiLine(s) がラベル自体も考慮してしまい、ラベル付きステートメント全体が複数行であると誤って判断してしまう点にありました。これにより、ラベル付きステートメントに続くコメントのインデントがずれていました。

この修正では、p.isMultiLine(s) の呼び出しを p.isMultiLine(unlabeledStmt(s)) に変更しています。

新しく追加された unlabeledStmt 関数は、以下のロジックで動作します。

// unlabeledStmt returns the statement of a labeled statement s;
// otherwise it return s.
func unlabeledStmt(s ast.Stmt) ast.Stmt {
	if s, _ := s.(*ast.LabeledStmt); s != nil {
		return unlabeledStmt(s.Stmt)
	}
	return s
}

この関数は、与えられた ast.Stmt*ast.LabeledStmt 型であるかどうかをチェックします。もしそうであれば、そのラベル付きステートメントの内部にある実際のステートメント(s.Stmt)を再帰的に unlabeledStmt に渡します。これにより、ネストされたラベル付きステートメントであっても、最終的に最も内側のラベルを持たないステートメントが返されます。もし与えられたステートメントがラベル付きステートメントでなければ、そのままそのステートメントを返します。

この変更により、stmtList メソッドがステートメントの複数行性を判断する際に、ラベル付きステートメントの「ラベル」部分を除外して、純粋な「ステートメント本体」の複数行性のみを考慮するようになります。結果として、ラベル付きステートメントに続くコメントは、ラベルの開始位置ではなく、実際のステートメント本体のインデントレベルに正しく揃えられるようになります。

テストケース src/pkg/go/printer/testdata/comments2.inputsrc/pkg/go/printer/testdata/comments2.golden が追加され、この修正が意図通りに動作することを確認しています。特に issue5623() 関数が追加され、様々なラベル付きステートメントとコメントの組み合わせで、コメントが正しくアライメントされることを検証しています。

コアとなるコードの変更箇所

src/pkg/go/printer/nodes.go の以下の部分が変更されました。

  1. stmtList メソッド内のコメントの修正:

    --- a/src/pkg/go/printer/nodes.go
    +++ b/src/pkg/go/printer/nodes.go
    @@ -906,7 +906,7 @@ func (p *printer) stmtList(list []ast.Stmt, nindent int, nextIsRBrace bool) {
     	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;
    +			// nindent == 0 only for lists of switch/select case clauses;
     			// in those cases each clause is a new section
     			if len(p.output) > 0 {
     				// only print line break if we are not at the beginning of the output
    @@ -914,7 +914,11 @@ func (p *printer) stmtList(list []ast.Stmt, nindent int, nextIsRBrace bool) {
     			}
     			p.stmt(s, nextIsRBrace && i == len(list)-1)
    -			multiLine = p.isMultiLine(s)
    +			// labeled statements put labels on a separate line, but here
    +			// we only care about whether the actual statement w/o label
    +			// is a multi-line statement - remove the label first
    +			// (was issue 5623)
    +			multiLine = p.isMultiLine(unlabeledStmt(s))
     			i++
     		}
     	}
    @@ -923,6 +927,15 @@ func (p *printer) stmtList(list []ast.Stmt, nindent int, nextIsRBrace bool) {
     	}
     }
     
    +// unlabeledStmt returns the statement of a labeled statement s;
    +// otherwise it return s.
    +func unlabeledStmt(s ast.Stmt) ast.Stmt {
    +	if s, _ := s.(*ast.LabeledStmt); s != nil {
    +		return unlabeledStmt(s.Stmt)
    +	}
    +	return s
    +}
    +
     // block prints an *ast.BlockStmt; it always spans at least two lines.
     func (p *printer) block(b *ast.BlockStmt, nindent int) {
     	p.print(b.Lbrace, token.LBRACE)
    
  2. 新しいヘルパー関数 unlabeledStmt の追加。

コアとなるコードの解説

  • src/pkg/go/printer/nodes.go:

    • stmtList 関数は、GoのASTノードのリストを処理し、整形されたコードを生成する主要な関数の一つです。この関数は、各ステートメントの後に改行を挿入するかどうか、およびそのステートメントが複数行にわたるかどうかを判断します。
    • 変更前の multiLine = p.isMultiLine(s) は、現在のステートメント s が複数行であるかを直接チェックしていました。しかし、s*ast.LabeledStmt の場合、ラベル自体が改行を伴うため、isMultiLinetrue を返してしまうことが問題でした。
    • 変更後の multiLine = p.isMultiLine(unlabeledStmt(s)) では、unlabeledStmt 関数を介して、ラベル付きステートメントから「ラベル」部分を取り除いた純粋なステートメント本体を取得し、その本体が複数行であるかをチェックするように修正されました。これにより、コメントのアライメントがラベルではなく、実際のコードのインデントに追従するようになります。
    • unlabeledStmt 関数は、再帰的にラベル付きステートメントを剥がしていくことで、最終的にラベルを持たないステートメントを返します。これは、L1: L2: stmt のように複数のラベルがネストされている場合にも正しく機能します。
  • src/pkg/go/printer/testdata/comments2.input および comments2.golden:

    • これらのファイルは、go/printer のテストデータです。.input ファイルは整形前のコード、.golden ファイルは整形後の期待されるコードを示します。
    • func issue5623() という新しいテストケースが追加されており、様々な長さのラベルと、行コメントおよびブロックコメントの組み合わせで、コメントが正しくアライメントされることを検証しています。これにより、このコミットが修正しようとしている具体的な問題が明確に示され、修正が期待通りに機能していることが確認できます。

関連リンク

参考にした情報源リンク

  • Go issue #5623 の内容
  • Go CL 68400043 の内容
  • Go言語の go/printer パッケージのドキュメント (GoDoc)
  • Go言語のASTに関する一般的な情報
  • Go言語のラベル付きステートメントに関する情報
  • gofmt ツールの動作原理に関する情報