[インデックス 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の仕組みに関する一般的な知識