[インデックス 1937] ファイルの概要
このコミットは、Go言語のドキュメンテーション生成システムの一部である docprinter.go
ファイルに対する機能拡張と改善を目的としています。具体的には、パッケージヘッダー、定数、変数、そして整形されたコメントの処理能力が向上しています。このファイルは、GoのソースコードからAST(抽象構文木)を解析し、人間が読みやすい形式、特にHTML形式でドキュメンテーションを生成するためのロジックを含んでいます。
コミット
More gds functionality:
- package headers
- constants
- variables
- formatted comments
Next steps:
- sorted output
- collection of all files belonging to a package
- fine-tuning of output
R=r OCL=26997 CL=26997
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6d5bba5148f57a28bc19d86f21ccb114e9aaf614
元コミット内容
commit 6d5bba5148f57a28bc19d86f21ccb114e9aaf614
Author: Robert Griesemer <gri@golang.org>
Date: Wed Apr 1 15:00:22 2009 -0700
More gds functionality:
- package headers
- constants
- variables
- formatted comments
Next steps:
- sorted output
- collection of all files belonging to a package
- fine-tuning of output
R=r
OCL=26997
CL=26997
---
usr/gri/pretty/docprinter.go | 301 ++++++++++++++++++++++++++++---------------\n 1 file changed, 200 insertions(+), 101 deletions(-)\n
変更の背景
このコミットは、Go言語の初期段階におけるドキュメンテーション生成ツールの開発の一環として行われました。コミットメッセージにある「gds functionality」は、Go Documentation System(GoDoc)の初期実装またはその前身を指していると考えられます。当時のGoDocはまだ発展途上にあり、より包括的で整形されたドキュメンテーションを生成するための機能が求められていました。
具体的には、以下の機能が不足していたため、それらを docprinter.go
に追加・改善する必要がありました。
- パッケージヘッダーの表示: パッケージ全体の概要やインポートパスをドキュメンテーションに含める機能。
- 定数と変数のドキュメンテーション: コード内で定義された定数や変数を適切に抽出し、そのコメントと共に表示する機能。
- 整形されたコメントの処理: コードコメントが単なるテキストとしてではなく、段落や整形済みテキストブロック(コード例など)として適切にHTMLに変換される機能。これにより、生成されるドキュメンテーションの可読性と表現力が大幅に向上します。
これらの機能追加は、Go言語のコードベースから自動的に高品質なドキュメンテーションを生成するというGoDocの基本的な目標を達成するために不可欠でした。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語および関連ツールの基本的な概念を理解しておく必要があります。
1. Go言語のAST (Abstract Syntax Tree)
Goコンパイラやツールは、Goのソースコードを直接処理するのではなく、まずそのコードを抽象構文木(AST)と呼ばれるツリー構造に変換します。ASTは、プログラムの構造を抽象的に表現したもので、各ノードがコードの要素(例えば、関数宣言、変数宣言、式、ステートメントなど)に対応します。
go/ast
パッケージ: Go標準ライブラリのgo/ast
パッケージは、GoソースコードのASTを表現するための型と関数を提供します。例えば、ast.File
は単一のGoソースファイルを表し、ast.Decl
は宣言(関数、型、変数、定数など)を表します。ast.Comments
: GoのコメントはASTの一部として扱われます。ast.Comments
は、特定の宣言やコードブロックに関連付けられたコメントのリストを表します。
2. Go言語の token
パッケージ
go/token
パッケージは、Go言語の字句解析(トークン化)で使われる定数(トークン)を定義します。例えば、token.CONST
は定数宣言のキーワード const
を、token.VAR
は変数宣言のキーワード var
を表します。
3. astprinter
(GoDocの内部ツール)
astprinter
は、GoのASTを整形して出力するための内部的なユーティリティです。このコミットの docprinter.go
は、この astprinter
を利用して、ASTからHTML形式のドキュメンテーションを生成しています。astprinter.Printer
は、ASTノードをトラバースし、指定されたフォーマットで出力する機能を提供します。
4. ドキュメンテーション生成の概念
プログラミング言語におけるドキュメンテーション生成ツール(例: Javadoc, Doxygen, GoDoc)は、ソースコード内の特定のコメントや構造から自動的にドキュメントを生成します。これにより、開発者はコードとドキュメントを同時に管理でき、ドキュメントの鮮度を保ちやすくなります。
- エクスポートされた識別子: Go言語では、識別子(関数名、変数名、型名など)が大文字で始まる場合、それはパッケージ外にエクスポート(公開)されます。GoDocのようなツールは通常、エクスポートされた識別子とその関連コメントをドキュメンテーションの対象とします。
- コメントの整形: ドキュメンテーションコメントは、単なるテキストではなく、段落、コードブロック、リストなど、構造化された情報として表示されることが望ましいです。そのため、ツールはコメント内の特定のパターン(例: インデントされた行はコードブロック、空行は段落の区切り)を解釈し、適切なHTMLタグ(
<p>
,<pre>
など)に変換する機能が必要です。
技術的詳細
このコミットは、usr/gri/pretty/docprinter.go
ファイルに大幅な変更を加えており、GoDocのドキュメンテーション生成ロジックを強化しています。主な変更点は以下の通りです。
1. PackageDoc
構造体の変更
PackageDoc
構造体は、単一のGoパッケージのドキュメンテーション全体を保持します。このコミットでは、以下のフィールドが追加・変更されました。
doc ast.Comments
: パッケージ全体のドキュメンテーションコメントを保持するためのフィールドが追加されました。これにより、ファイル先頭のパッケージコメントがドキュメンテーションに反映されるようになります。consts *vector.Vector
: 定数宣言のリストを保持するために、map[string] *constDoc
から*vector.Vector
に変更されました。vector.Vector
は動的な配列のようなもので、複数の定数ブロックを順序通りに保持できるようになります。vars *vector.Vector
: 変数宣言のリストを保持するために、同様にmap[string] *varDoc
から*vector.Vector
に変更されました。
これらの変更により、PackageDoc
はより柔軟に複数の定数/変数ブロックを管理し、パッケージ全体のドキュメンテーションコメントを保持できるようになりました。
2. addDecl
メソッドの拡張と ast.DeclList
の処理
PackageDoc.addDecl
メソッドは、ASTの宣言(ast.Decl
)を PackageDoc
に追加する役割を担います。このコミットでは、特に ast.DeclList
の処理が大幅に強化されました。
ast.DeclList
の導入: GoのASTでは、const (...)
やvar (...)
のように複数の定数や変数をまとめて宣言するブロックがast.DeclList
として表現されます。以前はast.ConstDecl
やast.VarDecl
が直接処理されていましたが、この変更によりast.DeclList
を適切に処理できるようになりました。token.CONST
とtoken.VAR
の処理:ast.DeclList
のTok
フィールド(token.Token
型)を使って、それが定数ブロック (token.CONST
) なのか変数ブロック (token.VAR
) なのかを判別し、それぞれdoc.consts
またはdoc.vars
に追加するロジックが追加されました。hasExportedDecls
関数: 新たに導入されたhasExportedDecls
関数は、ast.DeclList
内にエクスポートされた識別子が含まれているかをチェックします。これにより、公開された定数や変数のみがドキュメンテーションの対象となるように制御されます。
3. コメント整形ロジックの改善 (printComments
関数)
最も重要な変更の一つは、コメントの整形とHTMLへの変換ロジックの抜本的な見直しです。
htmlEscape
関数の改善: HTML特殊文字(<
,&
)をエスケープするhtmlEscape
関数が、string
から[]byte
を扱うように変更され、より効率的な処理が可能になりました。また、bytes.Buffer
を使用して文字列結合のオーバーヘッドを削減しています。untabify
関数の改善: 連続するタブ文字を単一のタブに変換するuntabify
関数も、string
から[]byte
を扱うように変更されました。これは、整形済みテキストブロック(<pre>
タグで囲まれる部分)の表示を改善するために重要です。stripCommentDelimiters
関数の導入: コメントの区切り文字(//
や/* ... */
)を取り除くためのstripCommentDelimiters
関数が追加されました。printComments
関数の導入とモード管理: 以前のprintComment
関数は単純な段落処理しか行いませんでしたが、新しいprintComments
関数は、コメントの内容に応じてHTMLの段落(<p>
)と整形済みテキストブロック(<pre>
)を動的に切り替える高度なロジックを導入しました。in_gap
,in_paragraph
,in_preformatted
という3つのモードを定義し、コメントの各行のインデント(タブ文字の有無)や空行に基づいてモードを遷移させます。- 行頭にタブがある場合は整形済みテキストブロック(
<pre>
)として扱い、それ以外は段落(<p>
)として扱います。 - 空行は段落の終了(
</p>
)または整形済みテキストブロック内の改行として処理されます。 - これにより、GoDocコメントでよく見られる、コード例をインデントして記述するスタイルが適切にHTMLに変換されるようになりました。
4. ドキュメンテーション出力の構造化
PackageDoc.Print
メソッド内のテンプレート処理が強化され、定数、変数、型、関数のセクションがより明確に構造化されて出力されるようになりました。
PROGRAM_HEADER-->
: パッケージのインポートパスとパッケージコメントが出力されるようになりました。CONSTANTS-->
,VARIABLES-->
:PackageDoc
に追加されたconsts
とvars
ベクターの内容がループ処理され、それぞれのconstDoc
やvarDoc
のprint
メソッドが呼び出されてHTMLに出力されます。これにより、定数と変数のドキュメンテーションが生成されます。TYPES-->
,FUNCTIONS-->
: 既存の型と関数の出力ロジックも、コメント整形ロジックの変更に合わせて更新されました。
これらの変更により、GoDocはよりリッチで構造化されたドキュメンテーションを生成できるようになり、Go言語のコードベースの可読性と理解度が向上しました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下の関数と構造体です。
-
PackageDoc
構造体の定義変更:--- a/usr/gri/pretty/docprinter.go +++ b/usr/gri/pretty/docprinter.go @@ -63,10 +75,10 @@ type typeDoc struct { type PackageDoc struct { name string; // package name - imports map[string] string; - consts map[string] *constDoc; + doc ast.Comments; // package documentation, if any + consts *vector.Vector; // list of *ast.DeclList with Tok == token.CONST + vars *vector.Vector; // list of *ast.DeclList with Tok == token.CONST types map[string] *typeDoc; - vars map[string] *varDoc; funcs map[string] *funcDoc; }
-
PackageDoc.addDecl
メソッド内のast.DeclList
処理:--- a/usr/gri/pretty/docprinter.go +++ b/usr/gri/pretty/docprinter.go @@ -165,24 +181,41 @@ func (doc *PackageDoc) addDecl(decl ast.Decl) { case *ast.DeclList: - for i, decl := range d.List { - doc.addDecl(decl); + switch d.Tok { + case token.IMPORT, token.TYPE: + for i, decl := range d.List { + doc.addDecl(decl); + } + case token.CONST: + if hasExportedDecls(d.List) { + doc.consts.Push(&constDoc{d}); + } + case token.VAR: + if hasExportedDecls(d.List) { + doc.consts.Push(&varDoc{d}); + } } } }
-
コメント整形ロジック (
printComments
関数とその周辺):--- a/usr/gri/pretty/docprinter.go +++ b/usr/gri/pretty/docprinter.go @@ -234,40 +292,108 @@ func stripWhiteSpace(s []byte) []byte { } -func cleanComment(s []byte) []byte { +func stripCommentDelimiters(s []byte) []byte { switch s[1] { - case '/': s = s[2 : len(s)-1]; - case '*': s = s[2 : len(s)-2]; - default : panic("illegal comment"); + case '/': return s[2 : len(s)-1]; + case '*': return s[2 : len(s)-2]; } - return stripWhiteSpace(s); + panic(); + return nil; } -func printComment(p *astPrinter.Printer, comment ast.Comments) { - in_paragraph := false; - for i, c := range comment { - s := cleanComment(c.Text);\n-\t\tif len(s) > 0 { - if !in_paragraph { - p.Printf("<p>\\n"); - in_paragraph = true; +const /* formatting mode */ ( + in_gap = iota; + in_paragraph; + in_preformatted; +) + +func printLine(p *astPrinter.Printer, line []byte, mode int) int { + indented := len(line) > 0 && line[0] == '\t'; + line = stripWhiteSpace(line); + if len(line) == 0 { + // empty line + switch mode { + case in_paragraph: + p.Printf("</p>\\n"); + mode = in_gap; + case in_preformatted: + p.Printf("\\n"); + // remain in preformatted + } + } else { + // non-empty line + if indented { + switch mode { + case in_gap: + p.Printf("<pre>\\n"); + case in_paragraph: + p.Printf("</p>\\n"); + p.Printf("<pre>\\n"); + } + mode = in_preformatted; + } else { + switch mode { + case in_gap: + p.Printf("<p>\\n"); + case in_preformatted: + p.Printf("</pre>\\n"); + p.Printf("<p>\\n"); + } + mode = in_paragraph; + } + // print line + p.Printf("%s\\n", untabify(htmlEscape(line))); + } + return mode; +} + + +func closeMode(p *astPrinter.Printer, mode int) { + switch mode { + case in_paragraph: + p.Printf("</p>\\n"); + case in_preformatted: + p.Printf("</pre>\\n"); + } +} + + +func printComments(p *astPrinter.Printer, comment ast.Comments) { + mode := in_gap; + for i, c := range comment { + s := stripCommentDelimiters(c.Text); + + // split comment into lines and print the lines + i0 := 0; // beginning of current line + for i := 0; i < len(s); i++ { + if s[i] == '\n' { + // reached line end - print current line + mode = printLine(p, s[i0 : i], mode); + i0 = i + 1; // beginning of next line; skip '\n' + } + } + + // print last line + mode = printLine(p, s[i0 : len(s)], mode); + } + closeMode(p, mode); +}
コアとなるコードの解説
1. PackageDoc
構造体の変更
PackageDoc
は、Goパッケージのドキュメンテーションを構築するための中心的なデータ構造です。
imports
フィールドが削除され、代わりにdoc ast.Comments
が追加されました。これは、パッケージレベルのドキュメンテーションコメント(通常、package
宣言の直前にあるコメント)を保持するためです。これにより、GoDocがパッケージ全体の概要を生成する際に、このコメントを利用できるようになります。consts
とvars
フィールドがmap[string] *constDoc
とmap[string] *varDoc
から*vector.Vector
に変更されました。map
はキーによる一意なアクセスを提供しますが、順序を保証しません。vector.Vector
は要素の順序を保持できるため、ソースコードでの宣言順に定数や変数をドキュメンテーションに表示することが可能になります。これは、ドキュメンテーションの自然な流れを保つ上で重要です。
2. PackageDoc.addDecl
メソッド内の ast.DeclList
処理
addDecl
メソッドは、GoのASTから個々の宣言(ast.Decl
)を受け取り、それを PackageDoc
構造体内の適切なフィールドに分類・追加します。
このコミットの重要な変更は、case *ast.DeclList:
ブロックです。
- Goのソースコードでは、
const ( ... )
やvar ( ... )
のように、複数の定数や変数を括弧で囲んでまとめて宣言する「宣言リスト」の構文があります。ASTでは、これがast.DeclList
として表現されます。 - 以前のコードでは
ast.DeclList
が適切に処理されていませんでしたが、この変更により、d.Tok
(トークンタイプ) をチェックすることで、それがtoken.CONST
(定数宣言リスト) なのかtoken.VAR
(変数宣言リスト) なのかを判別できるようになりました。 hasExportedDecls(d.List)
は、この宣言リスト内にエクスポートされた(つまり、大文字で始まる)識別子が含まれているかをチェックします。これにより、公開された定数や変数のみがドキュメンテーションの対象となり、内部的な定数や変数がドキュメントに漏れるのを防ぎます。- エクスポートされた宣言が含まれる場合、その
ast.DeclList
全体がconstDoc
またはvarDoc
オブジェクトとしてラップされ、それぞれdoc.consts
またはdoc.vars
ベクターに追加されます。これにより、宣言リスト全体が単一のドキュメンテーションブロックとして扱われ、その中の個々の定数や変数が適切に整形されて表示されるようになります。
3. コメント整形ロジック (printComments
関数とその周辺)
このセクションは、GoDocがコメントをHTMLに変換する際の「賢さ」を大幅に向上させるものです。
htmlEscape
とuntabify
の改善: これらの関数は、コメントテキストをHTMLに出力する前に前処理を行います。string
から[]byte
への変更は、Goにおける文字列操作の効率化(特に大量のテキスト処理においてメモリ割り当てとコピーを減らす)を目的としています。htmlEscape
はHTMLの特殊文字をエスケープし、untabify
は連続するタブを単一のタブに変換することで、整形済みテキストの表示を改善します。stripCommentDelimiters
: コメントの開始・終了デリミタ(//
,/*
,*/
)をテキストから取り除くシンプルなユーティリティです。printComments
関数とモード管理: これが最も重要な部分です。in_gap
,in_paragraph
,in_preformatted
という3つの定数は、コメントの現在の整形モードを表します。in_gap
: 空行やモードの開始・終了時など、コンテンツがない状態。in_paragraph
: 通常のテキスト段落。HTMLの<p>
タグで囲まれる。in_preformatted
: 整形済みテキスト(コード例など)。HTMLの<pre>
タグで囲まれる。
printComments
は、コメントブロックを1行ずつ処理し、各行の先頭にタブ文字があるかどうか(indented
)と、現在のモードに基づいて、出力するHTMLタグを動的に切り替えます。- インデントされた行の処理: 行頭にタブがある場合、それはコード例などの整形済みテキストと見なされます。現在のモードが
in_gap
またはin_paragraph
であれば、<pre>
タグを開始し、モードをin_preformatted
に変更します。既にin_preformatted
であれば、そのままテキストを出力します。 - 非インデント行の処理: 行頭にタブがない場合、それは通常の段落テキストと見なされます。現在のモードが
in_gap
またはin_preformatted
であれば、<p>
タグを開始し、モードをin_paragraph
に変更します。既にin_paragraph
であれば、そのままテキストを出力します。 - 空行の処理: 空行は、現在の段落を終了させる(
</p>
)か、整形済みテキストブロック内で改行(\n
)として扱われます。 - このロジックにより、GoDocコメントでよく使われる、コード例をタブでインデントして記述する慣習が、生成されるHTMLドキュメントで適切に
<pre>
タグとしてレンダリングされるようになります。これにより、ドキュメンテーションの可読性と表現力が飛躍的に向上しました。
関連リンク
- GoDoc: https://pkg.go.dev/cmd/go#hdr-Go_documentation
- Go ASTパッケージ: https://pkg.go.dev/go/ast
- Go Tokenパッケージ: https://pkg.go.dev/go/token
参考にした情報源リンク
- Go言語の公式ドキュメンテーション
- Go言語のソースコード(特に
go/doc
およびgo/ast
パッケージ) - GoDocの歴史に関する一般的な情報源(初期のGoDocの設計思想など)
- Go言語におけるコメントの慣習に関する情報源
- HTMLの
<p>
および<pre>
タグに関するMDN Web Docs