[インデックス 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言語の構文規則をパーサーの段階で厳密に適用し、コンパイルエラーを早期に検出するために非常に重要です。これにより、開発者はより正確なエラーメッセージを受け取ることができ、デバッグの労力を削減できます。
関連リンク
参考にした情報源リンク
特になし。