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

[インデックス 14001] ファイルの概要

このコミットは、Go言語のドキュメンテーションツール go/doc パッケージにおける、Example関数の出力コメントの処理に関する改善です。具体的には、go/doc がExample関数から合成する main 関数において、出力コメント(Output: で始まるコメント)が誤って main 関数のボディの一部として扱われないように修正し、Exampleの実行結果の検証をより正確に行えるようにします。

コミット

commit cf513387c3839e0815016ec8d9b4cf0cd1802aae
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Oct 2 08:35:20 2012 +1000

    go/doc: strip example output comment from synthesized main function
    
    R=gri
    CC=gobot, golang-dev
    https://golang.org/cl/6524047

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/cf513387c3839e0815016ec8d9b4cf0cd1802aae

元コミット内容

go/doc: strip example output comment from synthesized main function

変更の背景

Go言語のドキュメンテーションツール go/doc は、パッケージのドキュメントを生成する際に、Example関数(ExampleFoo のような命名規則に従う関数)を特別な方法で扱います。これらのExample関数は、単なるコードスニペットとして表示されるだけでなく、go test コマンドによって実際に実行され、その出力がドキュメントに埋め込まれた Output: コメントと一致するかどうかが検証されます。

この検証プロセスを可能にするため、go/doc パッケージはExample関数のコードを解析し、それを実行可能な main 関数を含むGoプログラムとして「合成」します。この合成された main 関数は、Example関数のボディをそのまま含みます。

しかし、Example関数の末尾に Output: コメントが存在する場合、以前の実装ではこのコメントが合成された main 関数の ast.BlockStmt (関数ボディを表す抽象構文木ノード) の End 位置に影響を与えていました。具体的には、exampleOutput 関数がExample関数の出力コメントを抽出する際に、関数の ast.FuncDeclPos()End() を基準にコメントを検索していました。これにより、Output: コメントが関数の構文木の一部として扱われ、playExample 関数が main 関数を合成する際に、このコメントが main 関数のボディの一部であるかのように見えてしまう可能性がありました。

この問題は、特にGo Playgroundのような環境でExampleコードを実行する際に顕在化します。Go Playgroundは、提供されたコードをコンパイルして実行し、その出力を表示します。もし Output: コメントが合成された main 関数のボディの一部として扱われると、Go Playgroundはそれを通常のコメントとしてではなく、コードの一部として解釈しようとするか、あるいは main 関数の実際の終了位置を誤認し、予期せぬコンパイルエラーや実行時エラーを引き起こす可能性がありました。

このコミットの目的は、go/doc がExample関数から main 関数を合成する際に、Output: コメントを main 関数のボディから適切に「剥ぎ取る」ことで、この問題を解決し、Exampleの実行と検証の正確性を保証することです。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。

  1. go/doc パッケージ: Go言語の標準ライブラリの一部であり、Goソースコードからドキュメンテーションを生成するためのツールを提供します。go doc コマンドや godoc サーバーの基盤となっています。このパッケージは、ソースコードの抽象構文木(AST)を解析し、パッケージ、関数、型、変数などのドキュメントを抽出します。

  2. Example関数: Go言語では、ExampleFoo のような命名規則を持つ関数は「Example関数」として特別に扱われます。これらの関数は、特定のパッケージや関数の使用例を示すために書かれ、go test コマンドによって実行されます。Example関数の末尾に // Output: で始まるコメントを記述することで、そのExampleの期待される出力を指定できます。go test はExampleを実行し、実際の出力と期待される出力を比較して、Exampleが正しく動作するかどうかを検証します。

    例:

    package mypackage
    
    import "fmt"
    
    func ExampleHello() {
        fmt.Println("Hello, World!")
        // Output: Hello, World!
    }
    
  3. go/ast パッケージ: Go言語の標準ライブラリの一部であり、Goソースコードの抽象構文木(AST: Abstract Syntax Tree)を表現するための型と関数を提供します。Goコンパイラや各種ツール(go/docgoimports など)は、ソースコードをASTに変換して解析や操作を行います。

    • ast.File: Goソースファイル全体を表すASTノード。
    • ast.FuncDecl: 関数宣言を表すASTノード。
    • ast.BlockStmt: ブロックステートメント({ ... })を表すASTノード。関数のボディなどがこれに該当します。
    • ast.CommentGroup: コメントのグループを表すASTノード。
  4. go/token パッケージ: Go言語の標準ライブラリの一部であり、Goソースコードのトークン(キーワード、識別子、演算子など)とソースコード内の位置情報(行番号、列番号など)を定義します。

  5. Go Playground: Go言語の公式ウェブサイトで提供されているオンラインツールで、Goコードをブラウザ上で記述、コンパイル、実行できます。Example関数は、godoc サーバー上でGo Playgroundの機能を使って実行可能なスニペットとして表示されることがあります。

技術的詳細

このコミットの主要な変更は、src/pkg/go/doc/example.go ファイル内の exampleOutput 関数と playExample 関数の修正に集中しています。

exampleOutput 関数の変更

exampleOutput 関数は、Example関数の期待される出力コメントを抽出する役割を担っています。変更前は、この関数は *ast.FuncDecl (関数宣言) を引数として受け取り、その Pos() (開始位置) と End() (終了位置) を基準にコメントを検索していました。

// 変更前
func exampleOutput(fun *ast.FuncDecl, comments []*ast.CommentGroup) string {
    // ...
    if cg.Pos() < fun.Pos() { // コメントが関数の開始位置より前にあるか
        continue
    }
    if cg.End() > fun.End() { // コメントが関数の終了位置より後にあるか
        break
    }
    // ...
}

このアプローチの問題点は、Output: コメントがExample関数の ast.FuncDeclEnd() 位置に影響を与える可能性があることです。つまり、コメントが関数の構文木の一部として扱われ、その結果、playExample 関数が main 関数を合成する際に、コメントが main 関数のボディの一部であるかのように見えてしまう可能性がありました。

このコミットでは、exampleOutput 関数の引数が *ast.FuncDecl から *ast.BlockStmt (関数ボディ) に変更されました。

// 変更後
func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string {
    // ...
    if cg.Pos() < b.Pos() { // コメントがブロックステートメントの開始位置より前にあるか
        continue
    }
    if cg.End() > b.End() { // コメントがブロックステートメントの終了位置より後にあるか
        break
    }
    // ...
}

この変更により、exampleOutput 関数はExample関数のボディ(ast.BlockStmt)の範囲内にあるコメントのみを検索するようになります。これにより、Output: コメントが関数のボディの論理的な範囲外にあると正しく認識され、後続の処理でボディの一部として誤って扱われることがなくなります。

playExample 関数の変更

playExample 関数は、Example関数をGo Playgroundで実行可能な形式に変換するために、Example関数のボディから新しい main 関数を合成する役割を担っています。

変更前は、playExample 関数はExample関数のボディをそのまま main 関数のボディとして使用していました。

// 変更前
// Synthesize main function.
funcDecl := &ast.FuncDecl{
    Name: ast.NewIdent("main"),
    Type: &ast.FuncType{},
    Body: body, // Example関数のボディをそのまま使用
}

このコミットでは、playExample 関数に、合成された main 関数のボディから Output: コメントを明示的に「剥ぎ取る」ロジックが追加されました。

// 変更後
// Strip "Output:" commment and adjust body end position.
if len(comments) > 0 {
    last := comments[len(comments)-1]
    if outputPrefix.MatchString(last.Text()) {
        comments = comments[:len(comments)-1] // 最後のコメント(Output:コメント)を削除
        // Copy body, as the original may be used elsewhere.
        body = &ast.BlockStmt{
            Lbrace: body.Pos(),
            List:   body.List,
            Rbrace: last.Pos(), // ボディの終了位置をOutput:コメントの開始位置に調整
        }
    }
}

// Synthesize main function.
funcDecl := &ast.FuncDecl{
    Name: ast.NewIdent("main"),
    Type: &ast.FuncType{},
    Body: body, // 調整されたボディを使用
}

この新しいロジックは以下のステップを実行します。

  1. コメントの確認: playExample 関数が処理するコメントリスト(Example関数に関連するコメント)の最後にコメントが存在するかを確認します。
  2. Output: コメントの検出: 最後のコメントが outputPrefix (正規表現 (?i)^[[:space:]]*output:) にマッチするかどうかを確認します。これは、Output: で始まるコメントを検出するためのものです。
  3. コメントの削除: もし最後のコメントが Output: コメントであれば、そのコメントをコメントリストから削除します。
  4. ボディの調整: 重要なのは、main 関数のボディ (ast.BlockStmt) を新しく作成し、その Rbrace (右中括弧の開始位置) を、削除された Output: コメントの Pos() (開始位置) に設定することです。これにより、合成された main 関数のボディは、Output: コメントを含まないように、その論理的な終了位置が調整されます。body をコピーしているのは、元の body が他の場所で参照されている可能性があるためです。

この変更により、go/doc がExample関数から合成する main 関数は、Output: コメントを完全に排除したクリーンなコードとなり、Go Playgroundのような環境での実行時に誤解釈されることがなくなります。

コアとなるコードの変更箇所

src/pkg/go/doc/example.go ファイルにおける変更は以下の通りです。

--- a/src/pkg/go/doc/example.go
+++ b/src/pkg/go/doc/example.go
@@ -61,7 +61,7 @@ func Examples(files ...*ast.File) []*Example {
 			Code:     f.Body,
 			Play:     playExample(file, f.Body),
 			Comments: file.Comments,
-			Output:   exampleOutput(f, file.Comments),
+			Output:   exampleOutput(f.Body, file.Comments),
 		})
 	}
 	if !hasTests && numDecl > 1 && len(flist) == 1 {
@@ -78,14 +78,14 @@ func Examples(files ...*ast.File) []*Example {

 var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`)

-func exampleOutput(fun *ast.FuncDecl, comments []*ast.CommentGroup) string {
+func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string {
 	// find the last comment in the function
 	var last *ast.CommentGroup
 	for _, cg := range comments {
-		if cg.Pos() < fun.Pos() {
+		if cg.Pos() < b.Pos() {
 			continue
 		}
-		if cg.End() > fun.End() {
+		if cg.End() > b.End() {
 			break
 		}
 		last = cg
@@ -163,8 +163,6 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 		}
 	}

-	// TODO(adg): look for other unresolved identifiers and, if found, give up.
-
 	// Synthesize new imports.
 	importDecl := &ast.GenDecl{
 		Tok:    token.IMPORT,
@@ -179,12 +177,7 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 		importDecl.Specs = append(importDecl.Specs, s)
 	}

-	// Synthesize main function.
-	funcDecl := &ast.FuncDecl{
-		Name: ast.NewIdent("main"),
-		Type: &ast.FuncType{},
-		Body: body,
-	}
+	// TODO(adg): look for other unresolved identifiers and, if found, give up.

 	// Filter out comments that are outside the function body.
 	var comments []*ast.CommentGroup
@@ -195,6 +188,27 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 		comments = append(comments, c)
 	}

+	// Strip "Output:" commment and adjust body end position.
+	if len(comments) > 0 {
+		last := comments[len(comments)-1]
+		if outputPrefix.MatchString(last.Text()) {
+			comments = comments[:len(comments)-1]
+			// Copy body, as the original may be used elsewhere.
+			body = &ast.BlockStmt{
+				Lbrace: body.Pos(),
+				List:   body.List,
+				Rbrace: last.Pos(),
+			}
+		}
+	}
+
+	// Synthesize main function.
+	funcDecl := &ast.FuncDecl{
+		Name: ast.NewIdent("main"),
+		Type: &ast.FuncType{},
+		Body: body,
+	}
+
 	// Synthesize file.
 	f := &ast.File{
 		Name:     ast.NewIdent("main"),

コアとなるコードの解説

exampleOutput 関数のシグネチャ変更と内部ロジックの調整

  • 変更前: func exampleOutput(fun *ast.FuncDecl, comments []*ast.CommentGroup) string
  • 変更後: func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string

この変更は、exampleOutput 関数がExample関数の期待される出力コメントを検索する際の「範囲」をより正確に定義することを目的としています。

  • 変更前は、関数宣言全体 (*ast.FuncDecl) の開始位置 (fun.Pos()) と終了位置 (fun.End()) を基準にコメントを検索していました。これには、関数宣言のシグネチャ部分や、関数ボディの後に続くコメントも含まれる可能性がありました。
  • 変更後は、関数ボディ (*ast.BlockStmt) の開始位置 (b.Pos()) と終了位置 (b.End()) を基準にコメントを検索します。これにより、Output: コメントがExample関数の実際のコードブロックの末尾に付随している場合にのみ、それが関連する出力コメントとして認識されるようになります。これは、Output: コメントがExample関数の論理的な「出力」を記述するものであり、コードの実行範囲と密接に関連しているべきであるという設計思想に基づいています。

playExample 関数における Output: コメントの除去ロジックの追加

playExample 関数は、Example関数をGo Playgroundで実行可能な形式に変換するために、Example関数のボディから新しい main 関数を合成します。このコミットの最も重要な変更は、この合成プロセスにおいて Output: コメントを明示的に除去するロジックが追加されたことです。

// Strip "Output:" commment and adjust body end position.
if len(comments) > 0 {
    last := comments[len(comments)-1]
    if outputPrefix.MatchString(last.Text()) {
        comments = comments[:len(comments)-1] // 最後のコメント(Output:コメント)を削除
        // Copy body, as the original may be used elsewhere.
        body = &ast.BlockStmt{
            Lbrace: body.Pos(),
            List:   body.List,
            Rbrace: last.Pos(), // ボディの終了位置をOutput:コメントの開始位置に調整
        }
    }
}

// Synthesize main function.
funcDecl := &ast.FuncDecl{
    Name: ast.NewIdent("main"),
    Type: &ast.FuncType{},
    Body: body, // 調整されたボディを使用
}

このコードブロックは、以下の目的で追加されました。

  1. Output: コメントの識別: comments リストの最後のコメントが outputPrefix (正規表現 (?i)^[[:space:]]*output:) にマッチするかどうかを確認します。これは、Example関数の期待される出力を示すコメントを正確に識別するためです。
  2. コメントリストからの削除: 識別された Output: コメントは、comments = comments[:len(comments)-1] によってコメントリストから削除されます。これにより、このコメントが合成される main 関数のコメントとして含まれることがなくなります。
  3. ast.BlockStmtRbrace 位置の調整: 最も重要なのは、body (Example関数のボディを表す ast.BlockStmt) を新しく作成し、その Rbrace (右中括弧の開始位置) を、削除された Output: コメントの Pos() (開始位置) に設定している点です。
    • ast.BlockStmt は、Lbrace (左中括弧の開始位置)、List (ステートメントのリスト)、Rbrace (右中括弧の開始位置) というフィールドを持ちます。
    • Rbrace: last.Pos() とすることで、合成される main 関数のボディの論理的な終了位置が、Output: コメントの開始位置に「切り詰められる」ことになります。これにより、Output: コメントは合成された main 関数のコードの一部として扱われなくなり、Go Playgroundのような環境でコードが実行される際に、このコメントがコードとして誤って解釈されることを防ぎます。
    • // Copy body, as the original may be used elsewhere. というコメントは、元の body オブジェクトが他の場所で参照されている可能性があるため、変更を加える前にコピーを作成していることを示しています。これは、意図しない副作用を防ぐための防御的なプログラミングです。

これらの変更により、go/doc がExample関数から生成する実行可能なコードは、Output: コメントを含まない、よりクリーンで正確なものとなり、Go Playgroundでの実行や go test による検証がより堅牢になります。

関連リンク

参考にした情報源リンク