[インデックス 14821] ファイルの概要
このコミットは、Go言語のドキュメンテーション生成ツールである go/doc
パッケージにおける Example
関数の処理ロジックを改善するものです。具体的には、Example
関数内で使用されるセレクタ式(例: fmt.Println
の fmt
部分)の解析方法を修正し、トップレベルの宣言への参照をより正確に検出できるようにします。これにより、go/doc
が Example
コードを正しく解析し、実行可能なプレイグラウンドコードを生成する際の精度が向上します。また、doc.Examples
のためのシンプルなテストが追加され、機能の検証が強化されています。
コミット
- コミットハッシュ:
60544b698ed1310dd3c5dbf67f73f29938d64e0d
- Author: Andrew Gerrand adg@golang.org
- Date: Mon Jan 7 19:36:38 2013 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/60544b698ed1310dd3c5dbf67f73f29938d64e0d
元コミット内容
go/doc: recursively inspect selector expressions
Also adds a couple of simple tests for doc.Examples.
Fixes #4561.
R=golang-dev, dave, rsc
CC=golang-dev
https://golang.org/cl/7067048
変更の背景
Go言語の go/doc
パッケージは、Goのソースコードからドキュメンテーションを生成する役割を担っています。このパッケージには、Example
関数という特別な関数を認識し、そのコードを抽出してドキュメンテーションに含める機能があります。さらに、これらの Example
関数は、Go Playgroundのような環境で実行可能なコードとして変換されることがあります。
Example
関数が正しく動作するためには、そのコードが自己完結型であるか、または外部のトップレベル宣言に依存しているかを正確に判断する必要があります。以前の実装では、Example
関数内のコードを抽象構文木(AST)として走査する際に、セレクタ式(例: fmt.Println
の fmt
のような、パッケージや構造体のフィールドにアクセスする式)の扱いが不完全でした。
具体的には、ast.Inspect
を用いてASTを走査する際、fmt.Println
のようなセレクタ式に遭遇した場合、Println
ではなく fmt
のみが未解決の識別子として認識されるべきでした。しかし、以前のコードでは、セレクタ式の左側(e.X
)を再帰的に検査していなかったため、fmt
のようなトップレベルのパッケージ名が正しく処理されず、Example
関数が外部依存していると誤って判断される可能性がありました。
この問題は、Go Playgroundで Example
コードを実行可能にする際に、必要なインポートや宣言が欠落したり、不正確なコードが生成されたりする原因となっていました。コミットメッセージにある Fixes #4561
は、このセレクタ式の不正確な処理に起因するバグを修正することを示しています。
前提知識の解説
1. Go言語の go/doc
パッケージ
go/doc
パッケージは、Goのソースコードからドキュメンテーションを生成するためのツールです。Goの標準ライブラリの一部であり、go doc
コマンドや godoc
サーバーの基盤となっています。このパッケージは、ソースコードを解析してパッケージ、関数、型、変数などのドキュメンテーションコメントを抽出し、構造化された形式で提供します。
特に、Example
関数(ExampleFoo
のような命名規則に従う関数)は、そのコードがドキュメンテーションに埋め込まれ、Go Playgroundで実行可能なスニペットとして表示されることがあります。
2. 抽象構文木 (Abstract Syntax Tree: AST)
ASTは、プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラやリンタ、コード分析ツールなどで広く利用されます。Go言語では、go/ast
パッケージがASTの構築と操作を提供します。
ast.File
: Goの単一のソースファイルを表すASTのルートノードです。ast.Node
: AST内の任意のノードを表すインターフェースです。ast.Inspect
: ASTを深さ優先で走査するためのユーティリティ関数です。指定されたノードから開始し、各ノードに対してコールバック関数を実行します。
3. セレクタ式 (Selector Expression)
Go言語におけるセレクタ式は、X.Sel
の形式で表現されます。ここで X
は式であり、Sel
は識別子です。
- パッケージ修飾子:
fmt.Println
のfmt
のように、パッケージ内のエクスポートされた識別子にアクセスする場合。 - 構造体フィールド:
myStruct.Field
のように、構造体のフィールドにアクセスする場合。 - インターフェースメソッド:
myInterfaceVar.Method()
のように、インターフェースのメソッドを呼び出す場合。
このコミットで問題となっていたのは、fmt.Println
のようなセレクタ式において、fmt
の部分が正しく解析されず、その fmt
がトップレベルの宣言(この場合は fmt
パッケージのインポート)に依存していることを検出できない可能性があった点です。
4. ast.Ident
と Obj
ast.Ident
: 識別子(変数名、関数名、型名など)を表すASTノードです。ast.Ident.Obj
: 識別子が解決されたオブジェクト(変数、関数、型など)へのポインタです。nil
の場合、その識別子は未解決であるか、またはトップレベルの宣言ではないことを示します。go/doc
はこのObj
フィールドを使って、Example
関数が外部のトップレベル宣言に依存しているかどうかを判断します。
技術的詳細
go/doc
パッケージの playExample
関数は、Example
関数内のコードを解析し、Go Playgroundで実行可能な形式に変換する準備をします。このプロセスの一環として、Example
関数が外部のトップレベル宣言(例えば、他のパッケージの関数やグローバル変数)に依存しているかどうかを検出する必要があります。もし依存している場合、その Example
は自己完結型ではないと判断され、Go Playgroundでの実行がサポートされないことがあります。
以前の実装では、playExample
関数内で ast.Inspect
を使用して Example
関数のボディ(body *ast.BlockStmt
)を走査し、未解決の識別子やトップレベル宣言への参照を検出していました。この走査ロジックは以下のようでした。
ast.Inspect(body, func(n ast.Node) bool {
// ...
if e, ok := n.(*ast.SelectorExpr); ok {
if id, ok := e.X.(*ast.Ident); ok && id.Obj == nil {
unresolved[id.Name] = true
}
return false // ここが問題
}
// ...
return true
})
このコードスニペットの return false
の部分が問題でした。ast.Inspect
のコールバック関数が false
を返すと、現在のノードの子ノードは走査されません。セレクタ式 e
(fmt.Println
など) に遭遇した際、e.X
(fmt
) が ast.Ident
であり、かつ id.Obj == nil
であれば、fmt
を未解決として unresolved
マップに追加していました。しかし、その後に return false
してしまうため、e.X
自体(この場合は fmt
)の内部構造がさらに検査されることはありませんでした。
これは、fmt.Println
のような単純なケースでは問題ないように見えますが、より複雑なセレクタ式、例えば pkg.SubPkg.Func()
のようなネストされたセレクタ式の場合、pkg.SubPkg
の部分が適切に検査されない可能性がありました。go/doc
が本当に知りたいのは、セレクタ式の最も左側の部分(例: fmt
や pkg
)がトップレベルの宣言であるかどうかです。
このコミットは、この return false
の挙動を修正し、セレクタ式の左側 (e.X
) を再帰的に検査するように変更することで、この問題を解決しています。これにより、go/doc
は Example
関数内のすべての識別子参照を正確に追跡し、自己完結性の判断をより正確に行えるようになりました。
コアとなるコードの変更箇所
変更は src/pkg/go/doc/example.go
ファイルの playExample
関数に集中しています。
--- a/src/pkg/go/doc/example.go
+++ b/src/pkg/go/doc/example.go
@@ -148,13 +148,13 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
// Find unresolved identifiers and uses of top-level declarations.
unresolved := make(map[string]bool)
usesTopDecl := false
- ast.Inspect(body, func(n ast.Node) bool {
- // For an expression like fmt.Println, only add "fmt" to the
- // set of unresolved names.
+ var inspectFunc func(ast.Node) bool
+ inspectFunc = func(n ast.Node) bool {
+ // For selector expressions, only inspect the left hand side.
+ // (For an expression like fmt.Println, only add "fmt" to the
+ // set of unresolved names, not "Println".)
if e, ok := n.(*ast.SelectorExpr); ok {
- if id, ok := e.X.(*ast.Ident); ok && id.Obj == nil {
- unresolved[id.Name] = true
- }
+ ast.Inspect(e.X, inspectFunc)
return false
}
if id, ok := n.(*ast.Ident); ok {
@@ -165,7 +165,8 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
}
}
return true
- })
+ }
+ ast.Inspect(body, inspectFunc)
if usesTopDecl {
// We don't support examples that are not self-contained (yet).
return nil
また、src/pkg/go/doc/example_test.go
という新しいテストファイルが追加されています。
コアとなるコードの解説
変更の核心は、playExample
関数内の ast.Inspect
の使い方にあります。
-
匿名関数から名前付き関数への変更: 以前は
ast.Inspect(body, func(n ast.Node) bool { ... })
のように匿名関数を直接渡していました。 変更後、inspectFunc
という名前付きの関数変数として定義されています。var inspectFunc func(ast.Node) bool inspectFunc = func(n ast.Node) bool { // ... } ast.Inspect(body, inspectFunc)
この変更の理由は、セレクタ式 (
ast.SelectorExpr
) の処理内でinspectFunc
自身を再帰的に呼び出す必要があるためです。匿名関数では自分自身を直接参照することができませんが、名前付き関数変数にすることで再帰呼び出しが可能になります。 -
セレクタ式の再帰的検査: 最も重要な変更は、
ast.SelectorExpr
を処理する部分です。 以前:if e, ok := n.(*ast.SelectorExpr); ok { if id, ok := e.X.(*ast.Ident); ok && id.Obj == nil { unresolved[id.Name] = true } return false // ここで子ノードの走査を停止していた }
変更後:
if e, ok := n.(*ast.SelectorExpr); ok { ast.Inspect(e.X, inspectFunc) // セレクタ式の左側 (e.X) を再帰的に検査 return false // 現在のセレクタ式ノード自体の子ノードは検査しないが、e.X は検査済み }
この
ast.Inspect(e.X, inspectFunc)
が追加されたことで、fmt.Println
のfmt
のようなセレクタ式の左側が、inspectFunc
によって再帰的に検査されるようになりました。これにより、fmt
がast.Ident
であり、そのObj
がnil
である場合に、unresolved
マップに正しく追加されるようになります。return false
はそのままですが、これはセレクタ式ノード自体の子ノード(この場合はPrintln
の部分)を検査する必要がないためです。go/doc
が関心があるのは、セレクタ式のルートとなる識別子(例:fmt
)がトップレベルの宣言であるかどうかだからです。 -
新しいテストファイルの追加:
src/pkg/go/doc/example_test.go
が追加され、doc.Examples
関数の動作を検証するテストケースが含まれています。ExampleHello
とExampleImport
という2つのテストケースが定義されており、それぞれシンプルなfmt.Println
の使用と、os/exec
のような外部パッケージのインポートを含む例を扱っています。- これらのテストは、
go/doc
がExample
関数を正しく解析し、期待されるPlay
コード(Go Playgroundで実行されるコード)とOutput
を生成するかどうかを検証します。 - 特に
ExampleImport
は、外部パッケージのインポートが正しく処理されることを確認するためのものであり、セレクタ式の解析の正確性が重要であることを示唆しています。
これらの変更により、go/doc
は Example
関数内のセレクタ式をより正確に解析し、未解決の識別子やトップレベル宣言への依存関係を適切に検出できるようになりました。これにより、Example
コードの正確なドキュメンテーション生成と、Go Playgroundでの信頼性の高い実行が可能になります。
関連リンク
- Go CL (Change List): https://golang.org/cl/7067048
参考にした情報源リンク
- Go言語の
go/ast
パッケージのドキュメンテーション - Go言語の
go/doc
パッケージのドキュメンテーション - Go言語のセレクタ式に関する一般的な情報
- GitHubのGoリポジトリにおけるIssue #4561 (検索を試みましたが、直接的な情報は見つかりませんでした。非常に古いIssueであるか、または別のプラットフォームで管理されていた可能性があります。)
- Go Playgroundの仕組みに関する一般的な知識