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

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

このコミットは、Go言語のgo/printerパッケージにおける、異なるファイルから構築されたAST(抽象構文木)の整形時のインデント処理に関する修正です。具体的には、ノードの位置情報に含まれるファイル名が変更された際に、プリンタがインデントをリセットする既存のヒューリスティックが、構造的に正しいASTに対して誤ったインデントを引き起こす可能性があった問題を解決しています。

コミット

commit de58eb9091d24abd9d837b8a787ba90eadd1ab0a
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Nov 16 13:17:12 2012 -0800

    go/printer: leave indentation alone when printing nodes from different files
    
    ASTs may be created by various tools and built from nodes of
    different files. An incorrectly constructed AST will likely
    not print at all, but a (structurally) correct AST with bad
    position information should still print structurally correct.
    
    One heuristic used was to reset indentation when the filename
    in the position information of nodes changed. However, this
    can lead to wrong indentation for structurally correct ASTs.
    
    Fix: Don't change the indentation in this case.
    
    Related to issue 4300.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6849066

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

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

元コミット内容

go/printer: 異なるファイルからのノードをプリントする際にインデントをそのままにする

ASTは様々なツールによって作成され、異なるファイルのノードから構築されることがあります。誤って構築されたASTは全くプリントされない可能性が高いですが、位置情報が不正な(しかし構造的には正しい)ASTは、構造的には正しくプリントされるべきです。

使用されていたヒューリスティックの一つに、ノードの位置情報に含まれるファイル名が変更された際にインデントをリセットするというものがありました。しかし、これは構造的に正しいASTに対して誤ったインデントを引き起こす可能性がありました。

修正: このケースではインデントを変更しない。

Issue 4300に関連。

変更の背景

Go言語のgo/printerパッケージは、Goのソースコードを整形して出力する役割を担っています。このパッケージは、Goのコンパイラやツールチェーンの一部として、AST(抽象構文木)を読み込み、それを整形されたGoコードとして出力します。

コミットメッセージによると、この問題は、複数のソースファイルから構築されたASTを処理する際に発生していました。Goのツール(例えばast.MergePackageFilesのようなもの)は、異なるファイルのASTを結合して一つの大きなASTを形成することがあります。この結合されたAST内の各ノードは、元のソースファイルにおける位置情報(ファイル名、行番号、列番号など)を持っています。

go/printerの内部では、コードの整形を行う際に、現在のノードの位置情報と直前のノードの位置情報を比較し、特にファイル名が変更された場合に特定の処理を行うヒューリスティックが導入されていました。このヒューリスティックは、ファイル名が変わった際にインデントをリセット(p.indent = 0)することで、新しいファイルのコードが適切に整形されることを意図していたと考えられます。

しかし、このインデントリセットのロジックが、意図しない副作用を引き起こしていました。具体的には、構造的には完全に正しいASTであるにもかかわらず、ノードの位置情報が異なるファイルを参照している場合に、インデントが不適切にリセットされてしまい、結果として出力されるコードのインデントが崩れる、あるいは「インデントアンダーフロー」(インデントレベルが負になるような状況)を引き起こす可能性がありました。これは、特にASTが複数のソースファイルからマージされたり、あるいはツールによって生成されたりする際に、位置情報が必ずしもプリンタの期待する「ファイルごとの連続性」を保たない場合に顕在化しました。

この問題は、Goのコード整形ツールが、ユーザーが期待するような正確で一貫した出力を提供できないという点で、ユーザーエクスペリエンスに影響を与えていました。コミットメッセージに「Related to issue 4300」とあることから、この問題は以前から認識されており、特定のバグ報告や議論の対象となっていたことが伺えます。(ただし、公開されているGoのIssue Trackerでは「Issue 4300」は確認できませんでした。これは内部的なトラッキング番号であるか、非常に古いIssueである可能性があります。)

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  1. AST (Abstract Syntax Tree - 抽象構文木): プログラミング言語のソースコードを、その構文構造に基づいて抽象化したツリー構造のデータ表現です。コンパイラやインタープリタは、ソースコードを直接処理するのではなく、まずASTに変換してから、そのASTを解析・最適化・コード生成などに利用します。Go言語では、go/astパッケージがASTの表現と操作を提供します。各ノードは、元のソースコードの特定の要素(変数宣言、関数呼び出し、制御構造など)に対応し、そのノードがソースコードのどこに位置していたかを示す「位置情報」(ファイル名、行番号、列番号、オフセットなど)を持つことが一般的です。

  2. go/printerパッケージ: Go言語の標準ライブラリの一部であり、GoのASTを整形されたGoのソースコードとして出力する機能を提供します。これは、go fmtコマンドの基盤となるパッケージであり、Goコードの自動整形において中心的な役割を担っています。go/printerは、ASTの構造を走査し、Goの公式なスタイルガイド(gofmtのスタイル)に従って、適切なインデント、改行、スペースなどを挿入しながらコードを生成します。

  3. token.Position: go/tokenパッケージで定義されている構造体で、ソースコード内の特定の位置を表します。これには、ファイル名 (Filename)、行番号 (Line)、列番号 (Column)、ファイル内でのバイトオフセット (Offset) などの情報が含まれます。ASTの各ノードは、通常、このtoken.Position情報を持っており、元のソースコードのどの部分に対応するかを示します。

  4. ヒューリスティック: 厳密なアルゴリズムではなく、経験則や試行錯誤に基づいて問題を解決するためのアプローチやルールを指します。このコミットの文脈では、「ファイル名が変わったらインデントをリセットする」というルールがヒューリスティックとして機能していました。これは、通常の場合にはうまく機能するが、特定の例外的な状況(今回のケースのように、異なるファイルからのノードがマージされたASTなど)では問題を引き起こす可能性がある、という性質を持っています。

  5. インデントアンダーフロー: コードのインデントレベルが、本来ありえない負の値になるような状況を指します。これは、インデントを減らす操作が、現在のインデントレベルよりも大きく行われた場合に発生する可能性があります。go/printerのような整形ツールでは、インデントレベルを内部的に管理しており、この値が不正になると、出力されるコードの整形が大きく崩れる原因となります。

技術的詳細

このコミットの技術的な核心は、go/printerパッケージ内のprinter構造体のwriteStringメソッドにおけるインデント処理の変更です。

writeStringメソッドは、Goのソースコードを整形して出力する際に、文字列(コードの一部)を書き込む役割を担っています。このメソッドは、書き込む文字列のtoken.Position情報を受け取ります。

変更前のコードでは、以下のロジックが存在しました。

if p.last.IsValid() && p.last.Filename != pos.Filename {
    p.indent = 0
    p.mode = 0
    p.wsbuf = p.wsbuf[0:0]
}

このコードは、直前に処理したノードの位置情報(p.last)が有効であり、かつ現在のノードの位置情報(pos)のファイル名が直前のノードのファイル名と異なる場合、以下の処理を行っていました。

  • p.indent = 0: インデントレベルを0にリセットします。これは、新しいファイルの内容を処理する際に、インデントを最初からやり直すことを意図していました。
  • p.mode = 0: プリンタの内部モードをリセットします。
  • p.wsbuf = p.wsbuf[0:0]: ホワイトスペースバッファをクリアします。

このヒューリスティックは、単一のファイル内のASTを処理する場合には問題ないことが多かったのですが、複数のファイルからマージされたASTを処理する際に問題を引き起こしました。例えば、ast.MergePackageFilesのような関数によって、異なるファイルのASTが結合された場合、ASTの走査中にpos.Filenameが頻繁に切り替わる可能性があります。この切り替わりが発生するたびにp.indentが0にリセットされると、本来はインデントが維持されるべきコードブロック(例えば、関数本体や構造体の定義など)であっても、インデントが失われ、結果として出力されるコードの整形が崩れてしまうのです。

コミットの修正は、このp.indent = 0の行を削除し、代わりに詳細なコメントを追加することによって行われました。

// Note: Do not set p.indent to 0 - this seems to be a bad heuristic.
//       ASTs may be created by various tools and built from nodes of
//       different files. An incorrectly constructed AST will likely
//       not print at all, but a (structurally) correct AST with bad
//       position information should still print structurally correct.
//       If p.indent is reset, indentation may be off, and likely lead
//       to indentation underflow (to detect set: debug = true).
//       See also issue 4300 (11/16/2012).

この変更により、ファイル名が変更された場合でも、p.indentはリセットされなくなりました。これにより、go/printerは、異なるファイルからのノードを含むASTであっても、その構造に基づいて適切なインデントを維持できるようになります。コメントは、この変更の理由と、インデントリセットが引き起こす可能性のある問題(インデントアンダーフローなど)を明確に説明しています。

この修正は、go/printerがより堅牢になり、様々なソースから生成されたASTに対しても、より正確なコード整形を提供できるようになることを意味します。

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

変更はsrc/pkg/go/printer/printer.goファイルの一箇所のみです。

--- a/src/pkg/go/printer/printer.go
+++ b/src/pkg/go/printer/printer.go
@@ -225,7 +225,14 @@ func (p *printer) writeString(pos token.Position, s string, isLit bool) {
 		// (used when printing merged ASTs of different files
 		// e.g., the result of ast.MergePackageFiles)
 		if p.last.IsValid() && p.last.Filename != pos.Filename {
-			p.indent = 0
+			// Note: Do not set p.indent to 0 - this seems to be a bad heuristic.
+			//       ASTs may be created by various tools and built from nodes of
+			//       different files. An incorrectly constructed AST will likely
+			//       not print at all, but a (structurally) correct AST with bad
+			//       position information should still print structurally correct.
+			//       If p.indent is reset, indentation may be off, and likely lead
+			//       to indentation underflow (to detect set: debug = true).
+			//       See also issue 4300 (11/16/2012).
 			p.mode = 0
 			p.wsbuf = p.wsbuf[0:0]
 		}

コアとなるコードの解説

変更されたのは、printer構造体のwriteStringメソッド内の条件分岐です。

元のコードでは、p.last.IsValid() && p.last.Filename != pos.Filenameという条件が真(つまり、直前のノードが有効で、かつ現在のノードのファイル名が直前のノードのファイル名と異なる)の場合に、p.indent = 0という行が実行されていました。

このコミットでは、このp.indent = 0の行が削除され、その代わりに複数行にわたるコメントが追加されています。このコメントは、なぜこの行が削除されたのか、そしてなぜインデントをリセットするヒューリスティックが「悪いヒューリスティック」であると判断されたのかを詳細に説明しています。

コメントの要点は以下の通りです。

  • 「Do not set p.indent to 0 - this seems to be a bad heuristic.」: p.indentを0に設定すべきではない。これは悪いヒューリスティックである。
  • 「ASTs may be created by various tools and built from nodes of different files.」: ASTは様々なツールによって作成され、異なるファイルのノードから構築される可能性がある。
  • 「An incorrectly constructed AST will likely not print at all, but a (structurally) correct AST with bad position information should still print structurally correct.」: 誤って構築されたASTは全くプリントされない可能性が高いが、位置情報が不正な(しかし構造的には正しい)ASTは、構造的には正しくプリントされるべきである。
  • 「If p.indent is reset, indentation may be off, and likely lead to indentation underflow (to detect set: debug = true).」: もしp.indentがリセットされると、インデントがずれる可能性があり、インデントアンダーフローを引き起こす可能性が高い。
  • 「See also issue 4300 (11/16/2012).」: Issue 4300も参照のこと。

この変更により、go/printerは、ファイル名の変更をインデントリセットのトリガーとして使用しなくなり、ASTの構造に基づいたより一貫性のあるインデントを維持するようになりました。p.modep.wsbufのリセットは引き続き行われますが、これらはインデントの直接的な問題とは異なり、ファイル境界を越える際の内部状態のクリーンアップに関連するものと考えられます。

関連リンク

参考にした情報源リンク

  • 上記のGitHubコミットページおよびGerritの変更リンク
  • Go言語のgo/astパッケージ、go/tokenパッケージ、go/printerパッケージのドキュメント(Go言語の公式ドキュメント)
  • AST(抽象構文木)に関する一般的なプログラミング言語の概念
  • (注: コミットメッセージに記載されている「issue 4300」は、Goの公開Issue Trackerでは見つかりませんでした。これは内部的なトラッキング番号であるか、非常に古いIssueである可能性があります。)