[インデックス 1914] ファイルの概要
このコミットは、Go言語のツールチェインにおけるコード整形(pretty printing)とドキュメンテーション生成の機能に大幅な改善と分離をもたらします。具体的には、抽象構文木(AST)の出力とドキュメンテーションの出力をそれぞれ専用のモジュールに分離し、より厳密な構文チェックをパーサーに導入しています。これにより、コードベースのモジュール性が向上し、将来的な機能拡張や保守が容易になります。
コミット
commit 8971cf23541e9f567a502e7c681b9e8684d97679
Author: Robert Griesemer <gri@golang.org>
Date: Mon Mar 30 17:13:11 2009 -0700
daily snapshot:
- separating printing of AST and documentation
- astprinter: will subsume ast printing functionality of printer
- docprinter: will subsume doc printing functionality of printer
also: more logic to collect all the documentation pertaining
to all files of a package
- parser: some cleanups, stricter syntax checks
- gds: hooks to test new doc printer (disabled)
R=r
OCL=26915
CL=26915
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8971cf23541e9f567a502e7c681b9e8684d97679
元コミット内容
daily snapshot:
- separating printing of AST and documentation
- astprinter: will subsume ast printing functionality of printer
- docprinter: will subsume doc printing functionality of printer
also: more logic to collect all the documentation pertaining
to all files of a package
- parser: some cleanups, stricter syntax checks
- gds: hooks to test new doc printer (disabled)
R=r
OCL=26915
CL=26915
変更の背景
このコミットが行われた2009年3月は、Go言語がまだ一般に公開される前の開発初期段階にあたります。当時のGo言語のツールチェイン、特にコードの整形やドキュメンテーション生成を担う部分は、単一のprinter
モジュールに集約されていたと考えられます。このような設計は、機能が限定的であれば問題ありませんが、Go言語の進化とともに、より複雑なASTの整形要件や、リッチなドキュメンテーション生成の必要性が高まっていました。
単一のprinter
モジュールがASTの整形とドキュメンテーション生成の両方を担当していると、以下のような問題が生じます。
- 関心の分離の欠如: ASTの構造を忠実に再現する整形と、人間が読みやすいドキュメンテーションを生成する整形では、目的と要件が異なります。これらが混在していると、コードが複雑になり、変更が困難になります。
- 保守性の低下: 一つのモジュールに多くの機能が詰め込まれていると、バグ修正や新機能の追加が他の機能に影響を与えるリスクが高まります。
- 拡張性の制限: 例えば、HTML形式のドキュメンテーションや、特定のフォーマットでのASTダンプなど、新しい出力形式を追加する際に、既存のコードを大幅に修正する必要が生じます。
また、パーサーの観点からは、開発初期段階では構文解析の厳密性がまだ十分でなかった可能性があります。Go言語の構文が固まるにつれて、より堅牢で正確なエラー検出が求められるようになります。
これらの背景から、本コミットでは以下の目的を達成しようとしています。
- AST整形とドキュメンテーション生成の責任を明確に分離し、それぞれの専門モジュール(
astprinter
とdocprinter
)を導入する。 docprinter
において、パッケージ内の全ファイルからドキュメンテーションを収集するロジックを強化する。- パーサーの構文チェックをより厳密にし、不正なコードパターンを早期に検出する。
- これらの変更を、Go言語のWebベースのドキュメンテーションサーバー(
gds
)でテストするためのフックを設ける。
これにより、Go言語のツールチェインはより堅牢で、保守しやすく、将来の機能拡張に対応しやすい基盤を築くことになります。
前提知識の解説
このコミットの理解を深めるためには、以下のGo言語およびコンパイラ関連の基本的な概念を理解しておく必要があります。
1. Go言語のAST (Abstract Syntax Tree)
Go言語のソースコードは、コンパイラによってまず字句解析(Lexical Analysis)され、トークン(token
パッケージで定義されるような、キーワード、識別子、演算子などの最小単位)のストリームに変換されます。次に、構文解析(Syntactic Analysis)によって、このトークンストリームが抽象構文木(AST)と呼ばれるツリー構造に変換されます。
ASTは、ソースコードの構文構造を抽象的に表現したものです。例えば、a + b
という式は、+
演算子をルートとするノードになり、その左右の子ノードがa
とb
という識別子になる、といった具合です。Go言語では、go/ast
パッケージがASTノードの定義を提供しています。
ASTは、コンパイラの様々な段階(型チェック、最適化、コード生成など)で利用されるだけでなく、go fmt
のようなコード整形ツールや、go doc
のようなドキュメンテーション生成ツールでも利用されます。
2. Go言語のドキュメンテーション
Go言語は、ソースコード内のコメントから自動的にドキュメンテーションを生成する仕組みを持っています。これは、エクスポートされた(大文字で始まる)パッケージ、関数、型、変数、定数などの宣言の直前にあるコメントブロックを解析することで行われます。go doc
コマンドや、Go言語の公式ドキュメンテーションサイト(pkg.go.dev
)で表示されるドキュメンテーションは、この仕組みによって生成されています。
ドキュメンテーションコメントは、通常、宣言の目的や使用方法を説明するために使用されます。Goのドキュメンテーションツールは、これらのコメントを解析し、整形された形で表示します。
3. Go言語のパーサー
Go言語のパーサーは、ソースコードを読み込み、その構文を解析してASTを構築する役割を担います。パーサーは、言語の文法規則に従ってトークンを組み合わせ、意味のある構造(式、ステートメント、宣言など)を認識します。構文エラーが発生した場合、パーサーはそれを検出し、適切なエラーメッセージを報告します。
このコミットでは、パーサー内の特定の関数が、入力されたASTノードが期待される構文カテゴリ(例えば「式」であるべきか、「型名」であるべきか)に属しているかをより厳密にチェックするように変更されています。
4. tabwriter
パッケージ
text/tabwriter
パッケージは、Go標準ライブラリの一部で、テキストをタブで揃えて整形するための機能を提供します。これは、表形式のデータを整形する際などに非常に便利です。このコミットでは、docprinter
がtabwriter
を利用して、生成されるドキュメンテーションのレイアウトを整えるために使用されています。
5. token
パッケージ
go/token
パッケージは、Go言語の字句解析器が生成するトークン(IDENT
、INT
、ADD
、LPAREN
など)の定数を定義しています。また、ソースコード内の位置情報(ファイル名、行番号、列番号、オフセット)を表すPosition
型も提供します。astprinter
は、これらのトークンや位置情報を使用して、ASTノードを正確に整形します。
6. ast
パッケージ
go/ast
パッケージは、Go言語の抽象構文木(AST)のノード構造を定義しています。例えば、ast.FuncDecl
は関数宣言を表し、ast.BinaryExpr
は二項演算子式を表します。astprinter
は、これらのASTノードをトラバースし、それぞれのノードのタイプに応じて適切な整形処理を行います。
技術的詳細
このコミットは、Go言語のコード整形とドキュメンテーション生成のアーキテクチャを根本的に改善するものです。主要な変更点は以下の3つの新しいGoファイルと、既存のパーサーの修正に集約されます。
1. usr/gri/pretty/astprinter.go
の新規追加
このファイルは、Go言語のASTを整形して出力するための専用のプリンタを実装しています。以前はprinter
モジュールがこの役割を担っていたと考えられますが、astprinter
はより詳細な制御と柔軟性を提供します。
-
Printer
構造体:text io.Write
: 出力先(例: 標準出力、ファイル、HTTPレスポンスライター)。html bool
: HTML出力モードの有効/無効を制御。full bool
: AST全体を出力するか、インターフェース(エクスポートされた要素のみ)を出力するかを制御。comments []*ast.Comment
: 関連付けられていないコメントのリスト。lastpos token.Position
: 最後に文字列が出力された後の位置。level int
,indentation int
: スコープレベルとインデントレベルを管理。separator int
: 保留中のセパレータ(空白、タブ、カンマ、セミコロン)の種類。newlines int
: 保留中の改行数。state int
: 現在のセマンティック状態(normal
,opening_scope
,closing_scope
,inside_list
)を管理し、フォーマットに影響を与える。prec int
: 現在の式の優先順位を管理し、括弧の追加を制御。
-
TaggedString
メソッド: このメソッドは、astprinter
の核となる出力ロジックを担います。- 保留中のセパレータ(空白、タブ、カンマ、セミコロン)を、現在のコンテキストに基づいて出力します。
- コメントをコードと適切にインターリーブして出力します。コメントのタイプ(
//
または/* */
)に応じて、インデントや空白の挿入を調整します。 - 保留中の改行を出力します。
- 指定された文字列を、オプションのタグ(HTMLタグなど)で囲んで出力します。
- 現在のセマンティック状態(スコープの開始/終了、リスト内など)を処理し、インデントレベルやスコープレベルを更新します。
-
DoXxx
メソッド群:astprinter
は、ast.Expr
、ast.Stmt
、ast.Decl
などのASTノードを訪問するためのVisit
メソッド(Goのast.Visitor
パターンに似ている)を実装しており、各ノードタイプに対応するDoBadExpr
、DoIdent
、DoBinaryExpr
、DoBlockStmt
、DoFuncDecl
などのメソッドを持っています。これらのメソッドは、それぞれのASTノードの構造を解析し、TaggedString
などの低レベルな出力メソッドを呼び出して、整形されたコードを生成します。例えば、DoBinaryExpr
は二項演算子の左右のオペランドと演算子を整形し、必要に応じて括弧を追加します。 -
Interface
およびProgram
メソッド:Interface
メソッドは、パッケージのエクスポートされた要素(関数、型、変数、定数)のみを整形して出力します。これは、ドキュメンテーション生成の際に利用されることを想定しています。一方、Program
メソッドは、パッケージ全体のASTを整形して出力します。
2. usr/gri/pretty/docprinter.go
の新規追加
このファイルは、Go言語のソースコードからドキュメンテーションを収集し、整形して出力するための専用のプリンタを実装しています。
-
PackageDoc
構造体:name string
: パッケージ名。imports map[string] string
: インポートされたパッケージ。consts map[string] *constDoc
: 定数のドキュメンテーション。types map[string] *typeDoc
: 型のドキュメンテーション(メソッドを含む)。vars map[string] *varDoc
: 変数のドキュメンテーション。funcs map[string] *funcDoc
: 関数のドキュメンテーション。 この構造体は、パッケージ内のすべてのドキュメンテーション関連情報を一元的に管理します。
-
Init
、AddPackage
、addDecl
メソッド:Init
:PackageDoc
構造体を初期化します。AddPackage
: 同じパッケージに属するソースファイルのAST(*ast.Package
)を受け取り、その中の宣言(ast.Decl
)をaddDecl
メソッドに渡してドキュメンテーション情報を収集します。addDecl
: 各宣言(定数、型、変数、関数など)を走査し、エクスポートされた(大文字で始まる)識別子に関連するドキュメンテーションコメントや構造情報をPackageDoc
構造体内の適切なマップに格納します。
-
Print
メソッド: 収集したドキュメンテーション情報を、HTMLテンプレート(template.html
)を使用して出力します。このメソッドは、astprinter.Printer
のインスタンスも内部的に使用し、コードスニペットの整形に利用します。
3. usr/gri/pretty/gds.go
の変更
gds.go
は、Go言語のWebベースのドキュメンテーションサーバー(Go Doc Server)の実装の一部です。このコミットでは、新しいdocprinter
をテストするためのフックが追加されています。
-
newdoc
フラグの追加:flag.Bool("newdoc", false, "use new document printing")
という新しいコマンドラインフラグが追加されました。これにより、ユーザーは新しいdocprinter
の動作を試すことができます。 -
serveFile
関数の変更:serveFile
関数は、GoソースファイルをHTTPレスポンスとして提供する役割を担っています。この関数内で、newdoc
フラグがtrue
の場合に、新しいdocprinter.PackageDoc
を使用してドキュメンテーションを生成し、tabwriter.NewWriter
を介して整形されたHTMLを出力するロジックが追加されました。これにより、新しいドキュメンテーション生成パイプラインが実際に動作するかを検証できるようになります。tabwriter
は、出力のタブ揃えを制御するために使用されます。
4. usr/gri/pretty/parser.go
の変更
パーサーは、ソースコードを解析してASTを構築する役割を担います。このコミットでは、パーサーの構文チェックがより厳密になるように修正されています。
-
makeXxx
関数からcheckXxx
関数へのリネームとロジックの強化:makeExpr
->checkExpr
: 式として有効なASTノードであるかをチェックします。特に、range
演算子がfor
ステートメントのトップレベルでのみ許可されるという制約が追加され、それ以外の場所で使用された場合はエラーとなります。makeTypeName
->checkTypeName
: 型名として有効なASTノードであるかをチェックします。makeCompositeLitType
->checkCompositeLitType
: 複合リテラルの型として有効なASTノードであるかをチェックします。makeExprOrType
->checkExprOrType
: 式または型として有効なASTノードであるかをチェックします。配列の長さが...
(Ellipsis)である場合にエラーを報告するロジックが追加されています。
これらの関数は、入力されたASTノードが期待される構文カテゴリに属しているかを
switch
文で詳細にチェックし、不正な場合はp.error_expected
を呼び出してエラーを報告し、*ast.BadExpr
を返します。これにより、パーサーはより早期に、より正確に構文エラーを検出できるようになります。 -
parseSimpleStmt
の変更:parseSimpleStmt
関数は、単純なステートメント(式ステートメント、代入ステートメントなど)を解析します。この関数にlabel_ok
という新しい引数が追加され、ラベル付きステートメントの解析ロジックが改善されました。これにより、パーサーはラベル付きステートメントをより正確に処理できるようになります。
これらの変更は、Go言語のツールチェインの内部構造を大幅に改善し、コードの品質、保守性、拡張性を高めるための重要なステップです。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルに集中しています。
-
usr/gri/pretty/astprinter.go
(新規追加):Printer
構造体の定義と、そのフィールド(text
,html
,full
,comments
,lastpos
,level
,indentation
,separator
,newlines
,state
,prec
)。Init
メソッド:Printer
の初期化。TaggedString
メソッド: ASTノードの出力におけるコメント、セパレータ、改行、状態遷移の処理。DoXxx
メソッド群: 各ASTノード(DoIdent
,DoBinaryExpr
,DoBlockStmt
,DoFuncDecl
など)の整形ロジック。Interface
およびProgram
メソッド: パッケージ全体の整形出力のエントリポイント。
-
usr/gri/pretty/docprinter.go
(新規追加):PackageDoc
構造体の定義と、そのフィールド(name
,imports
,consts
,types
,vars
,funcs
)。Init
メソッド:PackageDoc
の初期化。AddPackage
およびaddDecl
メソッド: ASTからドキュメンテーション情報を収集するロジック。Print
メソッド: 収集したドキュメンテーション情報をHTMLテンプレートで出力するロジック。
-
usr/gri/pretty/gds.go
:newdoc
フラグの追加。serveFile
関数内での、newdoc
フラグに基づく新しいdocprinter
の利用ロジック。
-
usr/gri/pretty/parser.go
:makeExpr
,makeTypeName
,makeCompositeLitType
,makeExprOrType
関数がそれぞれcheckExpr
,checkTypeName
,checkCompositeLitType
,checkExprOrType
にリネームされ、内部の構文チェックロジックが強化された点。parseSimpleStmt
関数にlabel_ok
引数が追加され、ラベル付きステートメントの解析が改善された点。
コアとなるコードの解説
astprinter.go
の TaggedString
メソッド
TaggedString
メソッドは、astprinter
の出力処理の中心です。このメソッドは、Goのソースコードを整形する際の複雑なルール(コメントの配置、空白の挿入、改行の制御など)を管理します。
func (P *Printer) TaggedString(pos token.Position, tag, s, endtag string) {
// ... (省略: 位置情報の推定) ...
// --------------------------------
// print pending separator, if any
// ... (セパレータの出力ロジック) ...
P.separator = none;
// --------------------------------
// interleave comments, if any
// ... (コメントのインターリーブロジック) ...
// --------------------------------
// interpret state
// ... (セマンティック状態の解釈とインデント調整) ...
// --------------------------------
// print pending newlines
// ... (改行の出力ロジック) ...
// --------------------------------
// print string
if *debug {
P.Printf("[%d]", pos);
}
P.Printf("%s%s%s", tag, P.htmlEscape(untabify(s)), endtag);
// --------------------------------
// interpret state
// ... (セマンティック状態の解釈とレベル調整) ...
P.laststate = P.state;
P.state = none;
// --------------------------------
// done
P.opt_semi = false;
pos.Offset += len(s); // rough estimate
pos.Column += len(s); // rough estimate
P.lastpos = pos;
}
このメソッドは、pos
(トークンの位置)、tag
(開始HTMLタグ)、s
(出力する文字列)、endtag
(終了HTMLタグ)を受け取ります。
- セパレータの処理:
P.separator
に設定された値(blank
,tab
,comma
,semicolon
など)に応じて、適切な空白や句読点を出力します。これにより、例えばa, b
のようにカンマの後に自動的に空白が挿入されたり、ステートメントの終わりにセミコロンが追加されたりします。 - コメントのインターリーブ:
P.comments
リストと現在のトークンの位置pos
を比較し、コメントが現在の文字列の前に来るべきであれば、コメントを整形して出力します。コメントのタイプ(//
または/* */
)や、コメントの前の空白の有無に応じて、インデントや改行を調整します。 - 改行の処理:
P.newlines
に設定された値に応じて、適切な数の改行を出力し、現在のインデントレベルに基づいてタブを挿入します。 - 文字列の出力: 最後に、
tag
、HTMLエスケープされたs
、endtag
を連結して出力します。untabify
関数は、文字列内の連続するタブを単一のタブに変換し、tabwriter
との連携を改善します。 - 状態遷移:
P.state
とP.laststate
を更新することで、次の出力に影響を与えるセマンティックなコンテキスト(例: スコープの開始/終了)を管理します。
このTaggedString
メソッドは、astprinter
内のすべてのDoXxx
メソッドから呼び出され、Goコードの整形における複雑なレイアウト規則を統一的に処理します。
parser.go
の checkExpr
メソッド
parser.go
におけるcheckExpr
(旧makeExpr
)メソッドは、構文解析中にASTノードが「式」として有効であるかを検証する役割を担います。
// checkExpr checks that x is an expression (and not a type).
func (p *parser) checkExpr(x ast.Expr) ast.Expr {
// TODO should provide predicate in AST nodes
switch t := x.(type) {
case *ast.BadExpr:
case *ast.Ident:
case *ast.IntLit:
case *ast.FloatLit:
case *ast.CharLit:
case *ast.StringLit:
case *ast.StringList:
case *ast.FunctionLit:
case *ast.CompositeLit:
case *ast.ParenExpr:
case *ast.SelectorExpr:
case *ast.IndexExpr:
case *ast.SliceExpr:
case *ast.TypeAssertExpr:
case *ast.CallExpr:
case *ast.StarExpr:
case *ast.UnaryExpr:
if t.Op == token.RANGE {
// the range operator is only allowed at the top of a for statement
p.error_expected(x.Pos(), "expression");
x = &ast.BadExpr{x.Pos()};
}
case *ast.BinaryExpr:
default:
// all other nodes are not proper expressions
p.error_expected(x.Pos(), "expression");
x = &ast.BadExpr{x.Pos()};
}
return x;
}
このメソッドは、引数x
として渡されたast.Expr
の具体的な型をswitch
文でチェックします。
*ast.BadExpr
、*ast.Ident
、各種リテラル(IntLit
,FloatLit
,CharLit
,StringLit
)、FunctionLit
、CompositeLit
、ParenExpr
、SelectorExpr
、IndexExpr
、SliceExpr
、TypeAssertExpr
、CallExpr
、StarExpr
、BinaryExpr
は、有効な式としてそのまま返されます。*ast.UnaryExpr
の場合、特にtoken.RANGE
演算子(for ... range
ループで使用される)が、for
ステートメントのトップレベル以外で使用されていないかをチェックします。もし不正な場所で使用されていれば、p.error_expected
を呼び出してエラーを報告し、*ast.BadExpr
(不正なASTノードを表す)に置き換えます。- 上記のいずれのケースにも当てはまらない場合(
default
)、そのASTノードは式として不適切であると判断し、エラーを報告して*ast.BadExpr
を返します。
このcheckExpr
メソッドや、同様のcheckTypeName
、checkCompositeLitType
、checkExprOrType
メソッドは、Go言語の構文規則をパーサーの段階で厳密に適用し、コンパイルエラーを早期に検出するために非常に重要です。これにより、開発者はより正確なエラーメッセージを受け取ることができ、デバッグの労力を削減できます。
関連リンク
参考にした情報源リンク
特になし。