Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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) の走査中に KeyValueExprKey.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 の内部処理では、NameValue といったキーが、あたかもコード内で定義されていない変数であるかのように扱われ、その結果、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 の部分。

KeyValueExprKeyValue の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 の処理にありました。KeyValueExprKeyValue の両方を持つ構造です。例えば、struct { Name string }{Name: "value"}Name: "value" の部分では、NameKey で、"value"Value です。

本来、KeyValueExprKey は、それが属する複合リテラル(例: 構造体リテラル)の型によって解決されるべきフィールド名であり、独立した識別子として扱われるべきではありませんでした。しかし、既存のAST走査ロジックでは、KeyValueExprKey も通常の ast.Ident と同様に処理され、その Nameunresolved マップに誤って追加されてしまっていました。

コミットメッセージの例では、UpperBound というキーが ast.KeyValueExprKey として存在し、これが未解決の識別子として誤って検出されていました。

 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 に代入され、oktrue になります。
  • ast.Inspect(e.Value, inspectFunc): もしノードが KeyValueExpr であった場合、この行が実行されます。ここで重要なのは、e.Value のみに対して ast.Inspect が再帰的に呼び出されている点です。つまり、KeyValueExpr の「値」の部分は引き続き走査されますが、「キー」の部分 (e.Key) は明示的に走査から除外されます。
  • return false: ast.Inspect のコールバック関数が false を返すと、現在のノードの子ノードに対するデフォルトの走査は停止します。これにより、KeyValueExprKey 部分が、その後の一般的な ast.Ident 処理によって誤って未解決の識別子として扱われることを防ぎます。Value 部分は上記の ast.Inspect(e.Value, inspectFunc) で明示的に走査されているため、問題ありません。

この変更により、KeyValueExpr のキーは、それが属する複合リテラルのコンテキストで正しく解決されるべきものとして扱われ、独立した未解決の識別子として誤ってフラグが立てられることがなくなります。結果として、KeyValueExpr を含むExampleコードも godoc でプレイアブルと判定されるようになります。

src/pkg/go/doc/example_test.go に追加されたテストケースは、KeyValueExpr を使用した様々なシナリオ(匿名構造体、flag.Flag のような標準ライブラリの型、トップレベルの変数宣言など)でExampleが正しくプレイアブルと判定されることを確認しています。

関連リンク

参考にした情報源リンク