[インデックス 14493] ファイルの概要
このコミットは、Go言語の標準ライブラリである go/printer
パッケージに対する機能拡張と改善を目的としています。具体的には、go/printer
が ast.Decl
(宣言) および ast.Stmt
(ステートメント) のリストを直接入力として受け入れ、整形できるようになりました。また、printer.Config
に Indent
フィールドが追加され、出力されるコード全体のベースとなるインデント量を設定できるようになっています。これにより、特定のコードブロックや部分的なコードスニペットをより柔軟に整形することが可能になります。
コミット
commit 2a982e8e25f35735268711d21c77aaaee75f8366
Author: Robert Griesemer <gri@golang.org>
Date: Mon Nov 26 13:14:04 2012 -0800
go/printer: Permit declaration and statement lists as input.
Also: Can set base indentation in printer.Config: all code
is going to be indented by at least that amount (except for
raw string literals spanning multiple lines, since their
values must not change).
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6847086
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2a982e8e25f35735268711d21c77aaaee75f8366
元コミット内容
go/printer: Permit declaration and statement lists as input.
Also: Can set base indentation in printer.Config: all code
is going to be indented by at least that amount (except for
raw string literals spanning multiple lines, since their
values must not change).
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6847086
変更の背景
Go言語の go/printer
パッケージは、Goのソースコードを整形(pretty-print)するためのツールです。これまでの go/printer
は、主に完全なGoのソースファイル(*ast.File
)を整形することを想定していました。しかし、GoのツールチェインやIDE、あるいはその他のコード生成ツールなどでは、ファイル全体ではなく、特定の宣言のリスト([]ast.Decl
)やステートメントのリスト([]ast.Stmt
)といった、より小さなASTノードの集合を整形したいというニーズがありました。
このコミットの背景には、以下のような課題と要望があったと考えられます。
- 部分的なコード整形への対応: Goのソースファイル全体ではなく、関数本体のステートメントリストや、トップレベルの宣言ブロックなど、コードの一部だけを整形したい場合に、
go/printer
を直接利用することが困難でした。これに対応するためには、一時的に*ast.File
構造を構築するなどの手間が必要でした。 - ベースインデントの制御: コードスニペットを整形する際、そのスニペットが最終的に配置されるコンテキストに応じて、全体に適用されるベースとなるインデント量を指定したいという要望がありました。例えば、関数本体のコードを整形する際に、その関数が既に特定のインデントレベルにある場合、そのインデントを考慮した整形が必要になります。
- ツールチェインの柔軟性向上:
go/printer
がより多様なASTノードの入力に対応することで、Goのツールチェイン(例:go fmt
、goimports
、各種IDEのフォーマッタ)がより柔軟に、かつ効率的に動作できるようになります。
これらの背景から、go/printer
のAPIを拡張し、より汎用的な整形機能を提供することが求められました。
前提知識の解説
このコミットを理解するためには、Go言語の以下の標準ライブラリに関する基本的な知識が必要です。
-
go/ast
パッケージ (Abstract Syntax Tree):go/ast
パッケージは、Goのソースコードを抽象構文木(AST)として表現するためのデータ構造を提供します。Goのコンパイラやツール(go fmt
など)は、ソースコードをまずASTにパースし、そのASTを操作することで様々な処理を行います。ast.File
: Goのソースファイル全体を表すASTノードです。パッケージ宣言、インポート、トップレベルの宣言(変数、定数、関数、型など)を含みます。ast.Decl
: 宣言(Declaration)を表すインターフェースです。ast.GenDecl
(汎用宣言: import, const, type, var)、ast.FuncDecl
(関数宣言) などがあります。ast.Stmt
: ステートメント(Statement)を表すインターフェースです。ast.ExprStmt
(式ステートメント)、ast.AssignStmt
(代入ステートメント)、ast.IfStmt
(ifステートメント)、ast.ForStmt
(forステートメント) など、Goのプログラムの実行可能な各行やブロックを構成する要素です。ast.Expr
: 式(Expression)を表すインターフェースです。リテラル、変数参照、関数呼び出しなどがあります。
-
go/token
パッケージ:go/token
パッケージは、Goのソースコード内のトークン(キーワード、識別子、演算子など)と、それらのソースコード上の位置情報(ファイル名、行番号、列番号、オフセット)を扱うための型と定数を提供します。token.FileSet
: 複数のソースファイルをまとめて管理し、各トークンの位置情報を一意に特定するためのコンテキストを提供します。token.Pos
: ソースコード内の特定の位置を表します。
-
go/printer
パッケージ:go/printer
パッケージは、go/ast
パッケージで表現されたASTを、Goの標準的なフォーマットルールに従って整形し、ソースコードとして出力する機能を提供します。go fmt
コマンドの内部でも利用されています。printer.Config
:go/printer
の整形動作を制御するための設定構造体です。タブ幅やインデントモードなどを指定できます。
これらのパッケージは、Goのソースコードをプログラム的に解析、操作、生成する上で不可欠な要素です。
技術的詳細
このコミットによる技術的な変更点は大きく分けて2つあります。
-
go/printer.Fprint
関数が[]ast.Decl
と[]ast.Stmt
を入力として受け入れるようになったこと:- 以前の
Fprint
は、*ast.File
、*CommentedNode
、または単一のast.Expr
,ast.Decl
,ast.Spec
,ast.Stmt
のみを直接整形できました。 - この変更により、
Fprint
のnode
引数に[]ast.Decl
(宣言のスライス) や[]ast.Stmt
(ステートメントのスライス) を渡すことができるようになりました。 - 内部的には、
printer.printNode
メソッドがこれらの新しい型 ([]ast.Stmt
,[]ast.Decl
) を処理するためのcase
を追加し、それぞれp.stmtList
およびp.declList
という新しいヘルパーメソッドを呼び出すように変更されています。 p.stmtList
とp.declList
は、それぞれのリスト内の要素をループ処理し、適切な改行やインデントを挿入しながら個々のステートメントや宣言を整形します。特に、p.stmtList
では、出力の先頭でない場合にのみ改行を挿入するロジックが追加されており、部分的なコード整形時の不要な改行を防いでいます。p.declList
は、宣言の種類が変わる場合や、次の宣言にドキュメントコメントが付いている場合に、トップレベルの宣言間に空行を挿入するロジックをカプセル化しています。これは元々p.file
メソッド内にあったロジックを分離したものです。
- 以前の
-
printer.Config
にIndent
フィールドが追加され、ベースインデントを設定できるようになったこと:printer.Config
構造体にIndent int
フィールドが追加されました。このフィールドのデフォルト値は0
です。Indent
フィールドに0
より大きい値を設定すると、go/printer
が出力するすべてのコード行(複数行にわたる生文字列リテラルを除く)が、その値の分だけ追加でインデントされます。- このベースインデントは、
printer.atLineBegin
メソッド内で適用されます。atLineBegin
は各行の先頭でインデントを書き込む役割を担っており、既存のp.indent
(AST構造に基づくインデント) に加えて、p.Config.Indent
の値が加算されてインデントが生成されます。 - 「複数行にわたる生文字列リテラル(raw string literals)」が例外とされるのは、これらのリテラルはGoのコード内で改行やインデントを含めて記述された内容がそのまま文字列値となるため、
go/printer
が勝手にインデントを追加すると、文字列リテラルの値が変わってしまうという問題があるためです。これはGoの言語仕様とgo/printer
の設計原則に基づいた重要な考慮事項です。
これらの変更により、go/printer
はより柔軟なコード整形機能を提供し、Goのツールチェインにおけるコード操作の可能性を広げています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更箇所は以下のファイルに集中しています。
-
src/pkg/go/printer/nodes.go
:func (p *printer) stmtList(...)
の変更:p.linebreak
の呼び出しにlen(p.output) > 0
の条件が追加され、出力の先頭でない場合にのみ改行が挿入されるようになりました。これにより、部分的なステートメントリストを整形する際に、先頭に不要な改行が入るのを防ぎます。
func (p *printer) file(src *ast.File)
の変更:- トップレベルの宣言リストを処理するロジックが
p.declList
という新しいメソッドに抽出されました。 p.file
は単にp.declList(src.Decls)
を呼び出すようになりました。
- トップレベルの宣言リストを処理するロジックが
func (p *printer) declList(list []ast.Decl)
の追加:- 宣言リストをループ処理し、宣言の種類変更やドキュメントコメントの有無に応じて適切な空行を挿入するロジックが実装されました。
-
src/pkg/go/printer/printer.go
:func (p *printer) atLineBegin(pos token.Position)
の変更:- インデントを書き込むループの回数が
p.indent
だけでなく、p.Config.Indent + p.indent
となり、Config
で指定されたベースインデントが適用されるようになりました。
- インデントを書き込むループの回数が
func (p *printer) printNode(node interface{}) error
の変更:case []ast.Stmt:
とcase []ast.Decl:
の新しいcase
が追加され、それぞれp.stmtList
とp.declList
を呼び出すようになりました。これにより、Fprint
がこれらの型の入力を直接処理できるようになります。
type Config struct
の変更:Indent int
フィールドが追加されました。
func (cfg *Config) Fprint(...)
のコメント変更:node
引数に[]ast.Decl
と[]ast.Stmt
が受け入れられるようになったことを示すようにドキュメントが更新されました。
-
src/pkg/go/printer/printer_test.go
:TestDeclLists
関数の追加:[]ast.Decl
をFprint
に渡して整形できることをテストします。
TestStmtLists
関数の追加:[]ast.Stmt
をFprint
に渡して整形できることをテストします。
TestBaseIndent
関数の追加:printer.Config.Indent
フィールドが正しく機能し、指定されたベースインデントがコード全体に適用されることを検証します。特に、複数行の生文字列リテラルがインデントされないことを確認するコメントが含まれています。
コアとなるコードの解説
src/pkg/go/printer/nodes.go
の変更
-
stmtList
の改行ロジック改善:// src/pkg/go/printer/nodes.go // ... func (p *printer) stmtList(list []ast.Stmt, nindent int, nextIsRBrace bool) { // ... if len(p.output) > 0 { // only print line break if we are not at the beginning of the output p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || nindent == 0 || multiLine) } // ... }
この変更は、
stmtList
が部分的なステートメントリストを整形する際に、出力バッファp.output
が空でない(つまり、これが最初の出力ではない)場合にのみ改行を挿入するようにします。これにより、例えば関数本体のステートメントだけを整形するような場合に、先頭に余分な改行が入るのを防ぎ、より自然な出力が得られます。 -
declList
メソッドの導入とfile
からの分離:// src/pkg/go/printer/nodes.go // ... func (p *printer) declList(list []ast.Decl) { tok := token.ILLEGAL for _, d := range list { prev := tok tok = declToken(d) // If the declaration token changed (e.g., from CONST to TYPE) // or the next declaration has documentation associated with it, // print an empty line between top-level declarations. if len(p.output) > 0 { // only print line break if we are not at the beginning of the output min := 1 if prev != tok || getDoc(d) != nil { min = 2 } p.linebreak(p.lineFor(d.Pos()), min, ignore, false) } p.decl(d) } } func (p *printer) file(src *ast.File) { p.setComment(src.Doc) p.print(src.Pos(), token.PACKAGE, blank) p.expr(src.Name) p.declList(src.Decls) // Call the new declList method p.print(newline) }
declList
は、トップレベルの宣言(ast.Decl
)のリストを整形するための新しいヘルパーメソッドです。以前はfile
メソッド内に直接記述されていた、宣言の種類(const
,type
,var
,func
)が変わる場合や、次の宣言にドキュメントコメントが付いている場合に空行を挿入するロジックがここに移動されました。これにより、コードの関心事が分離され、file
メソッドはより簡潔になりました。stmtList
と同様に、len(p.output) > 0
の条件で不要な先頭の改行を防いでいます。
src/pkg/go/printer/printer.go
の変更
-
ベースインデントの適用:
// src/pkg/go/printer/printer.go // ... func (p *printer) atLineBegin(pos token.Position) { // ... n := p.Config.Indent + p.indent // include base indentation for i := 0; i < n; i++ { p.output = append(p.output, '\t') } // ... }
atLineBegin
は、各行の先頭でインデントを書き込む際に呼び出されます。ここで、p.Config.Indent
(新しく追加されたベースインデント) が既存のp.indent
(AST構造に基づくインデント) に加算され、その合計値n
の分だけタブ文字 (\t
) が出力バッファに追加されます。これにより、Config.Indent
で指定された値が、出力されるコード全体の最小インデントとして機能します。 -
printNode
での新しい型 ([]ast.Stmt
,[]ast.Decl
) のハンドリング:// src/pkg/go/printer/printer.go // ... func (p *printer) printNode(node interface{}) error { // ... case []ast.Stmt: // A labeled statement will un-indent to position the label. // Set p.indent to 1 so we don't get indent "underflow". for _, s := range n { if _, ok := s.(*ast.LabeledStmt); ok { p.indent = 1 } } p.stmtList(n, 0, false) case []ast.Decl: p.declList(n) // ... }
printNode
は、printer.Fprint
に渡されたASTノードの型を判別し、適切な整形メソッドを呼び出す中心的なディスパッチャです。この変更により、[]ast.Stmt
と[]ast.Decl
という新しい型がcase
文で直接処理されるようになりました。それぞれ、新しく導入されたp.stmtList
とp.declList
メソッドが呼び出されます。[]ast.Stmt
のケースでは、ラベル付きステートメントのインデントに関する特別な処理(アンダーフローを防ぐためにp.indent
を1
に設定)も含まれています。 -
Config
構造体へのIndent
フィールド追加:// src/pkg/go/printer/printer.go // ... type Config struct { Mode Mode // default: 0 Tabwidth int // default: 8 Indent int // default: 0 (all code is indented at least by this much) }
printer.Config
にIndent
フィールドが追加されました。このフィールドは、出力されるコードに適用されるベースインデントのレベルをタブ数で指定します。
src/pkg/go/printer/printer_test.go
の変更
TestDeclLists
,TestStmtLists
,TestBaseIndent
の追加: これらのテストケースは、新しい機能が期待通りに動作することを確認します。TestDeclLists
とTestStmtLists
は、それぞれ宣言リストとステートメントリストをFprint
に渡して整形し、期待される出力と比較します。TestBaseIndent
は、異なるConfig.Indent
値を設定してprinter.go
ファイル自体を整形し、各行のインデントが指定されたベースインデント以上になっていることを検証します。特に、複数行の生文字列リテラルがインデントされないという例外ケースも考慮されています。
これらの変更は、go/printer
パッケージの汎用性と柔軟性を大幅に向上させ、Goのコード整形ツールやIDEの機能強化に貢献しています。
関連リンク
- Go Change-list (CL) 6847086: このコミットの元となったGoのコードレビューシステム上の変更リストです。詳細な議論やレビューコメントが含まれています。 https://golang.org/cl/6847086
参考にした情報源リンク
- Go言語の公式ドキュメント:
go/ast
パッケージ: https://pkg.go.dev/go/astgo/token
パッケージ: https://pkg.go.dev/go/tokengo/printer
パッケージ: https://pkg.go.dev/go/printer
- Go言語のソースコード:
go/printer
パッケージのソースコード: https://github.com/golang/go/tree/master/src/go/printergo/ast
パッケージのソースコード: https://github.com/golang/go/tree/master/src/go/ast
- Go言語のASTに関する解説記事:
- Go言語のASTについて解説しているブログ記事やチュートリアルは多数存在します。例えば、「Go AST」などで検索すると見つかります。