[インデックス 16270] ファイルの概要
このコミットは、Go言語のドキュメンテーションツール godoc
における、Exampleコードのプレイアビリティ(実行可能性)に関するバグ修正です。具体的には、KeyValueExpr
を含むExampleコードが godoc
上で正しく実行可能と判定されない問題を解決します。
コミット
commit a228e733b9908c3839cbca9c3545de0a3f1aba47
Author: Jeremiah Harmsen <jeremiah@google.com>
Date: Mon May 6 10:15:16 2013 -0700
go/doc/example: Fix bug causing false negatives for Example playability.
Allows Examples with KeyValue expressions to be playable in godoc.
During the traversal of the abstract syntax tree any KeyValueExpr Key.Name was incorrectly being added as an unresolved identifier.
Since this identifier could not be provided the Example was marked as unplayable.
This updates the AST traversal to skip Keys (but continue traversing the Values).
Example of problematic AST now fixed (see L99 where "UpperBound" was being added as a missing identifier):
81 . . . . . . . . . Values: []ast.Expr (len = 1) {
82 . . . . . . . . . . 0: *ast.UnaryExpr {
83 . . . . . . . . . . . OpPos: 12:19
84 . . . . . . . . . . . Op: &
85 . . . . . . . . . . . X: *ast.CompositeLit {\
86 . . . . . . . . . . . . Type: *ast.SelectorExpr {\
87 . . . . . . . . . . . . . X: *ast.Ident {\
88 . . . . . . . . . . . . . . NamePos: 12:20
89 . . . . . . . . . . . . . . Name: "t_proto"\
90 . . . . . . . . . . . . . }\
91 . . . . . . . . . . . . . Sel: *ast.Ident {\
92 . . . . . . . . . . . . . . NamePos: 12:41
93 . . . . . . . . . . . . . . Name: "BConfig"\
94 . . . . . . . . . . . . . }\
95 . . . . . . . . . . . . }\
96 . . . . . . . . . . . . Lbrace: 12:79
97 . . . . . . . . . . . . Elts: []ast.Expr (len = 2) {\
98 . . . . . . . . . . . . . 0: *ast.KeyValueExpr {\
99 . . . . . . . . . . . . . . Key: *ast.Ident {\
100 . . . . . . . . . . . . . . . NamePos: 13:3
101 . . . . . . . . . . . . . . . Name: "UpperBound"\
102 . . . . . . . . . . . . . . }\
103 . . . . . . . . . . . . . . Colon: 13:13
104 . . . . . . . . . . . . . . Value: *ast.CallExpr {\
105 . . . . . . . . . . . . . . . Fun: *ast.SelectorExpr {\
106 . . . . . . . . . . . . . . . . X: *ast.Ident {\
107 . . . . . . . . . . . . . . . . . NamePos: 13:15
108 . . . . . . . . . . . . . . . . . Name: "proto"\
109 . . . . . . . . . . . . . . . . }\
110 . . . . . . . . . . . . . . . . Sel: *ast.Ident {\
111 . . . . . . . . . . . . . . . . . NamePos: 13:21
112 . . . . . . . . . . . . . . . . . Name: "Float32"\
113 . . . . . . . . . . . . . . . . }\
R=adg
CC=gobot, golang-dev, gri
https://golang.org/cl/8569045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a228e733b9908c3839cbca9c3545de0a3f1aba47
元コミット内容
このコミットは、go/doc/example
パッケージにおけるバグ修正を目的としています。具体的には、KeyValueExpr
(キーと値のペアで構成される式、例えば構造体リテラルやマップリテラル内で使用される) を含むExampleコードが、godoc
によって「実行不可能 (unplayable)」と誤って判定される問題に対処しています。
問題の根源は、抽象構文木 (AST) の走査中に KeyValueExpr
の Key.Name
が誤って未解決の識別子として追加されていたことにありました。これにより、その識別子が解決できないため、Exampleが実行不可能とマークされていました。
この修正では、AST走査のロジックを更新し、KeyValueExpr
のキー部分はスキップし、値の部分のみを走査するように変更しています。これにより、キーが誤って未解決の識別子として扱われることがなくなり、KeyValueExpr
を含むExampleも godoc
で正しく実行可能と判定されるようになります。
コミットメッセージには、問題のあるASTの例が示されており、UpperBound
というキーが誤って欠落した識別子として追加されていたことが指摘されています。
変更の背景
Go言語の godoc
ツールは、Goのソースコードからドキュメンテーションを生成し、Exampleコードをインタラクティブに実行できる機能を提供します。この「プレイアブルなExample」機能は、ユーザーがドキュメントを読みながらコードの動作を実際に試すことができるため、非常に有用です。
しかし、特定のコード構造、特に struct
リテラルや map
リテラル内で使用される KeyValueExpr
を含むExampleコードが、godoc
によって正しく解析されず、実行可能と判定されないというバグが存在していました。これは、godoc
がExampleコードのASTを解析する際に、KeyValueExpr
のキー部分を誤って「未解決の識別子」として認識してしまっていたためです。
例えば、以下のようなコードがあったとします。
type Config struct {
Name string
Value int
}
func ExampleConfig() {
cfg := Config{
Name: "test",
Value: 123,
}
fmt.Println(cfg.Name)
// Output: test
}
この Name: "test"
や Value: 123
の部分が KeyValueExpr
です。godoc
の内部処理では、Name
や Value
といったキーが、あたかもコード内で定義されていない変数であるかのように扱われ、その結果、Example全体が実行不可能と判断されていました。
このバグは、KeyValueExpr
を多用するコードのExampleにおいて、godoc
の有用性を損なうものでした。そのため、この問題を修正し、より多くのExampleコードが godoc
で正しく機能するようにすることが求められました。
前提知識の解説
このコミットの理解には、以下のGo言語および関連ツールの概念に関する知識が役立ちます。
1. Go言語の抽象構文木 (AST)
Goコンパイラやツールは、Goのソースコードを直接処理するのではなく、まずそのコードを抽象構文木 (Abstract Syntax Tree, AST) と呼ばれるツリー構造に変換します。ASTは、プログラムの構造を抽象的に表現したもので、コードの各要素(変数、関数、式、ステートメントなど)がノードとして表現されます。
go/ast
パッケージ: Go標準ライブラリのgo/ast
パッケージは、GoのソースコードのASTを表現するための型と関数を提供します。例えば、ast.File
はGoのソースファイル全体を表し、ast.FuncDecl
は関数宣言を、ast.Expr
は式を表すインターフェースです。ast.Inspect
:go/ast
パッケージにはast.Inspect
という関数があり、これはASTを深さ優先で走査するための汎用的なメカニズムを提供します。この関数は、各ノードに対してユーザー定義の関数(func(n ast.Node) bool
)を呼び出し、そのノードをさらに走査するかどうかを制御できます。
2. ast.KeyValueExpr
ast.KeyValueExpr
は、GoのASTにおける特定の種類の式を表します。これは、キーと値のペアで構成される式で、主に以下のGoの構文で使用されます。
- 構造体リテラル:
MyStruct{Key: Value}
のKey: Value
の部分。 - マップリテラル:
map[KeyType]ValueType{Key: Value}
のKey: Value
の部分。
KeyValueExpr
は Key
と Value
の2つのフィールドを持ち、それぞれが ast.Expr
型です。
3. godoc
と Example
godoc
は、Goのソースコードからドキュメンテーションを生成し、Webブラウザで表示するツールです。godoc
は、パッケージ、関数、型などのドキュメントコメントを解析するだけでなく、Example関数も特別な方法で扱います。
- Example関数:
Example
で始まる関数(例:ExampleFprint
)は、そのパッケージのドキュメントにExampleコードとして表示されます。 - プレイアブルなExample:
godoc
は、特定の条件を満たすExample関数を「プレイアブル(実行可能)」として認識し、Webインターフェース上でそのコードを実行し、出力を表示する機能を提供します。これにより、ユーザーはドキュメントを読みながらコードの動作をインタラクティブに確認できます。 - 出力の検証: Example関数が
// Output:
コメントを含む場合、godoc
はExampleの実行結果がそのコメントの内容と一致するかどうかを検証します。
godoc
がExampleをプレイアブルと判断するためには、そのExampleコードが自己完結しており、未解決の識別子(定義されていない変数や関数など)を含まない必要があります。godoc
はExampleコードのASTを解析し、必要な依存関係や識別子がすべて解決可能であるかをチェックします。
4. 未解決の識別子 (Unresolved Identifiers)
プログラミングにおいて「識別子」とは、変数名、関数名、型名などを指します。「未解決の識別子」とは、コード内で使用されているにもかかわらず、そのスコープ内で定義が見つからない識別子のことです。これは通常、コンパイルエラーの原因となります。
godoc
がExampleのプレイアビリティをチェックする際、ExampleコードのASTを走査し、すべての識別子が解決可能であるかを確認します。もし未解決の識別子が見つかった場合、godoc
はそのExampleを実行できないと判断し、「unplayable」とマークします。
このコミットのバグは、KeyValueExpr
のキー部分が、本来はリテラルの構造の一部として解決されるべきであるにもかかわらず、誤って独立した未解決の識別子として扱われていたことに起因します。
技術的詳細
このバグは、go/doc/example
パッケージ内の playExample
関数がExampleコードのASTを走査し、未解決の識別子を検出するロジックに存在していました。
playExample
関数は、ast.Inspect
を使用してASTを再帰的に走査します。この走査中に、各ノードが ast.Ident
(識別子) 型であるかどうかをチェックし、もし id.Obj == nil
であれば、その識別子を未解決の識別子として unresolved
マップに追加していました。
問題は ast.KeyValueExpr
の処理にありました。KeyValueExpr
は Key
と Value
の両方を持つ構造です。例えば、struct { Name string }{Name: "value"}
の Name: "value"
の部分では、Name
が Key
で、"value"
が Value
です。
本来、KeyValueExpr
の Key
は、それが属する複合リテラル(例: 構造体リテラル)の型によって解決されるべきフィールド名であり、独立した識別子として扱われるべきではありませんでした。しかし、既存のAST走査ロジックでは、KeyValueExpr
の Key
も通常の ast.Ident
と同様に処理され、その Name
が unresolved
マップに誤って追加されてしまっていました。
コミットメッセージの例では、UpperBound
というキーが ast.KeyValueExpr
の Key
として存在し、これが未解決の識別子として誤って検出されていました。
98 . . . . . . . . . . . . . 0: *ast.KeyValueExpr {
99 . . . . . . . . . . . . . . Key: *ast.Ident {
100 . . . . . . . . . . . . . . . NamePos: 13:3
101 . . . . . . . . . . . . . . . Name: "UpperBound" <-- ここが問題
102 . . . . . . . . . . . . . . }\
この修正は、ast.Inspect
のコールバック関数内で、ノードが ast.KeyValueExpr
である場合に特別な処理を追加することで行われます。具体的には、KeyValueExpr
の場合は Key
部分の走査をスキップし、Value
部分のみを再帰的に走査するように変更します。これにより、Key
が誤って未解決の識別子として登録されることを防ぎます。
コアとなるコードの変更箇所
変更は src/pkg/go/doc/example.go
ファイルに集中しています。
--- a/src/pkg/go/doc/example.go
+++ b/src/pkg/go/doc/example.go
@@ -166,6 +166,13 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
ast.Inspect(e.X, inspectFunc)
return false
}
+ // For key value expressions, only inspect the value
+ // as the key should be resolved by the type of the
+ // composite literal.
+ if e, ok := n.(*ast.KeyValueExpr); ok {
+ ast.Inspect(e.Value, inspectFunc)
+ return false
+ }
if id, ok := n.(*ast.Ident); ok {
if id.Obj == nil {
unresolved[id.Name] = true
また、src/pkg/go/doc/example_test.go
には、この修正を検証するための新しいテストケースが追加されています。
--- a/src/pkg/go/doc/example_test.go
+++ b/src/pkg/go/doc/example_test.go
@@ -18,6 +18,7 @@ const exampleTestFile = `
package foo_test
import (
+\t"flag"\
\t"fmt"\
\t"log"\
\t"os/exec"\
@@ -35,6 +38,38 @@ func ExampleImport() {\
\t}\
\tfmt.Printf("The date is %s\\n", out)\
}\
+\n+func ExampleKeyValue() {\
+\tv := struct {\
+\t\ta string\n+\t\tb int\n+\t}{\
+\t\ta: "A",\n+\t\tb: 1,\n+\t}\
+\tfmt.Print(v)\
+\t// Output: a: "A", b: 1\n+}\
+\n+func ExampleKeyValueImport() {\
+\tf := flag.Flag{\
+\t\tName: "play",\n+\t}\
+\tfmt.Print(f)\
+\t// Output: Name: "play"\n+}\
+\n+var keyValueTopDecl = struct {\
+\ta string\n+\tb int\n+}{\
+\ta: "B",\n+\tb: 2,\
+}\n+\n+func ExampleKeyValueTopDecl() {\
+\tfmt.Print(keyValueTopDecl)\
+}\
`\
\n var exampleTestCases = []struct {\
@@ -49,6 +82,20 @@ var exampleTestCases = []struct {\
\t\tName: "Import",\n \t\tPlay: exampleImportPlay,\n \t},\
+\t{\n+\t\tName: "KeyValue",\n+\t\tPlay: exampleKeyValuePlay,\n+\t\tOutput: "a: \\"A\\", b: 1\\n",\n+\t},\
+\t{\n+\t\tName: "KeyValueImport",\n+\t\tPlay: exampleKeyValueImportPlay,\n+\t\tOutput: "Name: \\"play\\"\\n",\n+\t},\
+\t{\n+\t\tName: "KeyValueTopDecl",\n+\t\tPlay: "<nil>",\n+\t},\
}\
\n const exampleHelloPlay = `package main\
@@ -78,6 +125,39 @@ func main() {\
}\n `\
\n+const exampleKeyValuePlay = `package main\
+\n+import (\n+\t"fmt"\n+)\n+\n+func main() {\n+\tv := struct {\n+\t\ta string\n+\t\tb int\n+\t}{\n+\t\ta: "A",\n+\t\tb: 1,\n+\t}\n+\tfmt.Print(v)\n+}\n+`\
+\n+const exampleKeyValueImportPlay = `package main\
+\n+import (\n+\t"flag"\n+\t"fmt"\n+)\n+\n+func main() {\n+\tf := flag.Flag{\n+\t\tName: "play",\n+\t}\n+\tfmt.Print(f)\n+}\n+`\
+\n func TestExamples(t *testing.T) {\
\tfs := token.NewFileSet()\
\tfile, err := parser.ParseFile(fs, "test.go", strings.NewReader(exampleTestFile), parser.ParseComments)\
コアとなるコードの解説
src/pkg/go/doc/example.go
の変更は、playExample
関数内の inspectFunc
という無名関数(ast.Inspect
に渡されるコールバック関数)に新しい条件分岐を追加しています。
// For key value expressions, only inspect the value
// as the key should be resolved by the type of the
// composite literal.
if e, ok := n.(*ast.KeyValueExpr); ok {
ast.Inspect(e.Value, inspectFunc)
return false
}
このコードブロックは、ast.Inspect
が現在走査しているASTノード n
が *ast.KeyValueExpr
型であるかどうかをチェックします。
if e, ok := n.(*ast.KeyValueExpr); ok { ... }
: これは型アサーションとカンマOK構文を使用して、n
が*ast.KeyValueExpr
型に変換可能かどうかをチェックしています。変換可能であれば、その値は変数e
に代入され、ok
はtrue
になります。ast.Inspect(e.Value, inspectFunc)
: もしノードがKeyValueExpr
であった場合、この行が実行されます。ここで重要なのは、e.Value
のみに対してast.Inspect
が再帰的に呼び出されている点です。つまり、KeyValueExpr
の「値」の部分は引き続き走査されますが、「キー」の部分 (e.Key
) は明示的に走査から除外されます。return false
:ast.Inspect
のコールバック関数がfalse
を返すと、現在のノードの子ノードに対するデフォルトの走査は停止します。これにより、KeyValueExpr
のKey
部分が、その後の一般的なast.Ident
処理によって誤って未解決の識別子として扱われることを防ぎます。Value
部分は上記のast.Inspect(e.Value, inspectFunc)
で明示的に走査されているため、問題ありません。
この変更により、KeyValueExpr
のキーは、それが属する複合リテラルのコンテキストで正しく解決されるべきものとして扱われ、独立した未解決の識別子として誤ってフラグが立てられることがなくなります。結果として、KeyValueExpr
を含むExampleコードも godoc
でプレイアブルと判定されるようになります。
src/pkg/go/doc/example_test.go
に追加されたテストケースは、KeyValueExpr
を使用した様々なシナリオ(匿名構造体、flag.Flag
のような標準ライブラリの型、トップレベルの変数宣言など)でExampleが正しくプレイアブルと判定されることを確認しています。
関連リンク
- Go言語の
ast
パッケージ: https://pkg.go.dev/go/ast - Go言語の
go/doc
パッケージ: https://pkg.go.dev/go/doc - Go言語の
godoc
コマンド: https://pkg.go.dev/cmd/godoc - Go言語のExample関数に関する公式ドキュメント: https://go.dev/blog/examples
参考にした情報源リンク
- https://github.com/golang/go/commit/a228e733b9908c3839cbca9c3545de0a3f1aba47
- https://golang.org/cl/8569045 (Gerrit Code Review リンク)
- Go AST Explorer (ASTの構造を視覚的に確認できるツール): https://go.dev/play/?tab=ast
- Go言語の構造体リテラル: https://go.dev/ref/spec#Struct_literals
- Go言語のマップリテラル: https://go.dev/ref/spec#Map_literals