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

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

このコミットは、Go言語のドキュメンテーションツールであるgo/docおよびgodocにおける、Exampleコードの処理に関する修正です。具体的には、Exampleコード内で使用されるブランクインポート(_で始まるインポート)の取り扱いを改善し、ast.SortImportsを適用することで、生成されるExampleコードのインポートが正しくソートされるようにします。これにより、Exampleコードの表示がより正確で、Goの慣習に沿ったものになります。

コミット

commit 80dcc434a8a20ddc579810f88e770e098e7f9eb8
Author: Robert Griesemer <gri@golang.org>
Date:   Sat Nov 17 10:40:11 2012 -0800

    go/doc: fix identifier blank import handling for examples
    
    Replacement CL for 6813061; thanks to minux for prototyping.
    
    Fixes #4300.
    
    R=minux.ma
    CC=golang-dev
    https://golang.org/cl/6782082
---
 src/cmd/godoc/godoc.go    |  1 +
 src/pkg/go/doc/example.go | 34 +++++++++++++++++++++++-----------\n 2 files changed, 24 insertions(+), 11 deletions(-)

diff --git a/src/cmd/godoc/godoc.go b/src/cmd/godoc/godoc.go
index b72aad56c0..57ef9f3778 100644
--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -356,6 +356,7 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File
 		// (use tabs, no comment highlight, etc).
 		play := ""
 		if eg.Play != nil && *showPlayground {
+			ast.SortImports(fset, eg.Play)
 			var buf bytes.Buffer
 			err := (&printer.Config{Mode: printer.TabIndent, Tabwidth: 8}).Fprint(&buf, fset, eg.Play)
 			if err != nil {
diff --git a/src/pkg/go/doc/example.go b/src/pkg/go/doc/example.go
index 5c51ecef34..e5752bb15a 100644
--- a/src/pkg/go/doc/example.go
+++ b/src/pkg/go/doc/example.go
@@ -145,8 +145,9 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 	// Use unresolved identifiers to determine the imports used by this
 	// example. The heuristic assumes package names match base import
 	// paths. (Should be good enough most of the time.)
-	imports := make(map[string]string) // [name]path
-	for _, s := range file.Imports {
+	// paths for imports w/o renames (should be good enough most of the time).
+	namedImports := make(map[string]string) // [name]path
+	var blankImports []ast.Spec             // _ imports
+	for _, s := range file.Imports {
 		p, err := strconv.Unquote(s.Path.Value)
 		if err != nil {
 			continue
 		}
 		n := path.Base(p)
 		if s.Name != nil {
-			if s.Name.Name == "." {
+			n = s.Name.Name
+			switch n {
+			case "_":
+				blankImports = append(blankImports, s)
+				continue
+			case ".":
 				// We can't resolve dot imports (yet).
 				return nil
 			}
-			n = s.Name.Name
 		}
 		if unresolved[n] {
-			imports[n] = p
+			namedImports[n] = p
 			delete(unresolved, n)
 		}
 	}
@@ -172,13 +177,19 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 		return nil
 	}
 
-	// Filter out comments that are outside the function body.
+	// Include documentation belonging to blank imports.
 	var comments []*ast.CommentGroup
+	for _, s := range blankImports {
+		if c := s.(*ast.ImportSpec).Doc; c != nil {
+			comments = append(comments, c)
+		}
+	}
+
+	// Include comments that are inside the function body.
 	for _, c := range file.Comments {
-		if c.Pos() < body.Pos() || c.Pos() >= body.End() {
-			continue
+		if body.Pos() <= c.Pos() && c.End() <= body.End() {
+			comments = append(comments, c)
 		}
-		comments = append(comments, c)
 	}
 
 	// Strip "Output:" commment and adjust body end position.
@@ -190,13 +201,14 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 		Lparen: 1, // Need non-zero Lparen and Rparen so that printer
 		Rparen: 1, // treats this as a factored import.
 	}
-	for n, p := range imports {
+	for n, p := range namedImports {
 		s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
 		if path.Base(p) != n {
 			s.Name = ast.NewIdent(n)
 		}
 		importDecl.Specs = append(importDecl.Specs, s)
 	}
+	importDecl.Specs = append(importDecl.Specs, blankImports...)
 
 	// Synthesize main function.
 	funcDecl := &ast.FuncDecl{
@@ -213,7 +225,7 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 	}\n}\n\n-// playExample takes a whole file example and synthesizes a new *ast.File\n+// playExampleFile takes a whole file example and synthesizes a new *ast.File\n // such that the example is function main in package main.\n func playExampleFile(file *ast.File) *ast.File {\n 	// Strip copyright comment if present.\n```

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

https://github.com/golang/go/commit/80dcc434a8a20ddc579810f88e770e098e7f9eb8

## 元コミット内容

このコミットの元の内容は以下の通りです。

> go/doc: fix identifier blank import handling for examples
>
> Replacement CL for 6813061; thanks to minux for prototyping.
>
> Fixes #4300.
>
> R=minux.ma
> CC=golang-dev
> https://golang.org/cl/6782082

日本語にすると、「`go/doc`:Exampleにおける識別子としてのブランクインポートの取り扱いを修正。これはCL 6813061の代替であり、minuxのプロトタイピングに感謝する。Issue #4300を修正する。」となります。

## 変更の背景

このコミットの背景には、`go/doc`ツールがGoのExampleコードを処理する際に、ブランクインポート(`import _ "package/path"`)が適切に扱われていなかった問題があります。GoのExampleは、ドキュメンテーションの一部としてコードスニペットを実行可能にするためのもので、`godoc`コマンドや`go doc`コマンドで表示されます。

以前の実装では、Exampleコード内のブランクインポートが正しく認識されず、結果として生成されるExampleの実行可能コード(Playgroundで実行されるコードなど)から、ブランクインポートが欠落したり、インポートの順序がGoの標準的なフォーマット(`goimports`や`gofmt`によって適用されるもの)に従っていなかった可能性があります。

コミットメッセージにある`Fixes #4300`は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。また、`Replacement CL for 6813061`とあることから、以前にもこの問題に対する修正が試みられたものの、このコミットがそのより良い代替案として導入されたことがわかります。`minux`氏によるプロトタイピングがこの修正に貢献したことも言及されています。

この修正の目的は、`go/doc`が生成するExampleコードが、Goの言語仕様とツール(特に`goimports`や`gofmt`)が期待するインポートの取り扱いとフォーマットに準拠するようにすることです。これにより、ドキュメンテーションの品質とExampleの信頼性が向上します。

## 前提知識の解説

### Go言語のインポート

Go言語では、他のパッケージの機能を利用するために`import`文を使用します。

*   **通常のインポート**: `import "fmt"`のように、パッケージ名を指定してインポートします。
*   **エイリアスインポート**: `import io "fmt"`のように、パッケージに別名を付けてインポートします。
*   **ドットインポート**: `import . "fmt"`のように、パッケージ名を省略して、そのパッケージの識別子を直接参照できるようにします。これは通常、非推奨とされます。
*   **ブランクインポート (Blank Import)**: `import _ "database/sql"`のように、パッケージ名を`_`(アンダースコア)で指定するインポートです。これは、パッケージの初期化関数(`init()`関数)を実行するためだけにパッケージをインポートし、そのパッケージの識別子を直接使用しない場合に用いられます。例えば、データベースドライバを登録するためによく使われます。

### GoのAST (Abstract Syntax Tree)

Go言語のコンパイラやツールは、ソースコードを解析して抽象構文木(AST: Abstract Syntax Tree)を構築します。ASTは、ソースコードの構造を木構造で表現したものです。`go/ast`パッケージは、GoのソースコードのASTを操作するための機能を提供します。

*   `ast.File`: Goのソースファイル全体のASTを表す構造体です。
*   `ast.ImportSpec`: 個々のインポート文のASTを表す構造体です。
*   `ast.SortImports`: `go/ast`パッケージに含まれる関数で、Goのソースファイル内のインポート宣言をGoの標準的なルールに従ってソートし、重複を削除します。これは`gofmt`や`goimports`ツールが内部的に使用している機能です。

### `go/doc`パッケージと`godoc`コマンド

*   **`go/doc`パッケージ**: Goのソースコードからドキュメンテーションを生成するためのライブラリです。パッケージ、関数、型、変数、定数、そしてExampleコードなどの情報を抽出します。
*   **`godoc`コマンド**: `go/doc`パッケージを利用して、GoのパッケージドキュメンテーションをHTML形式やプレーンテキスト形式で表示するツールです。Goの標準ライブラリのドキュメンテーションサイト(pkg.go.devなど)も、このツールによって生成されています。Exampleコードは、この`godoc`によって表示され、Go Playgroundで実行可能な形式に変換されることがあります。

### Go Playground

Go Playgroundは、Goのコードをブラウザ上で記述、実行、共有できるウェブサービスです。`godoc`がExampleコードを表示する際に、このPlaygroundで実行可能な形式に変換して提供することがあります。

## 技術的詳細

このコミットは、`go/doc`パッケージがExampleコードを処理し、特にGo Playgroundで実行可能な形式に変換する際のロジックを改善します。

`go/doc`パッケージ内の`playExample`関数(変更後は`playExampleFile`にリネーム)は、ExampleコードのASTを受け取り、それを`package main`の`main`関数を含む完全なGoプログラムに変換します。この変換プロセスでは、Exampleコードが参照するパッケージのインポートを特定し、新しいASTに含める必要があります。

問題は、ブランクインポート(`import _ "path/to/package"`)が、通常のインポートとは異なる振る舞いをすることにありました。ブランクインポートは、そのパッケージの識別子を直接使用しないため、`playExample`関数が「未解決の識別子」を基にインポートを特定する既存のヒューリスティックでは、正しく検出されない可能性がありました。

このコミットの主な技術的変更点は以下の通りです。

1.  **ブランクインポートの明示的な収集**: `playExample`関数内で、通常の名前付きインポート(`namedImports`)とは別に、ブランクインポート(`blankImports`)を明示的に収集するようになりました。これにより、ブランクインポートがExampleのASTから誤って除外されることを防ぎます。
2.  **コメントの取り扱い**: ブランクインポートに付随するドキュメンテーションコメントも、生成されるExampleコードに含めるように修正されました。これにより、元のExampleコードの意図がより正確に反映されます。
3.  **インポートのソート**: `src/cmd/godoc/godoc.go`の`example_htmlFunc`関数内で、ExampleコードをPlayground形式で表示する直前に`ast.SortImports(fset, eg.Play)`が呼び出されるようになりました。これにより、生成されるExampleコードのインポートがGoの標準的な順序でソートされ、`gofmt`や`goimports`が適用されたかのような整形が保証されます。

これらの変更により、`go/doc`はExampleコード内のブランクインポートを正しく認識し、それらを最終的な実行可能コードに含めることができるようになります。また、インポートのソートにより、生成されるコードの可読性と一貫性が向上します。

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

このコミットによる主要なコード変更は以下の2つのファイルにあります。

1.  **`src/cmd/godoc/godoc.go`**:
    *   `example_htmlFunc`関数内で、Playgroundで表示するExampleコードを整形する前に、`ast.SortImports(fset, eg.Play)`が追加されました。

2.  **`src/pkg/go/doc/example.go`**:
    *   `playExample`関数(後に`playExampleFile`にリネーム)内で、インポートを処理するロジックが変更されました。
        *   `imports`マップが`namedImports`マップと`blankImports`スライスに分割されました。
        *   インポートをループする際に、`s.Name.Name == "_"`の場合にブランクインポートとして`blankImports`スライスに追加するロジックが追加されました。
        *   ブランクインポートに付随するドキュメンテーションコメントを収集するロジックが追加されました。
        *   最終的に生成される`ast.File`のインポート宣言に、`namedImports`に加えて`blankImports`も追加されるようになりました。
        *   関数名が`playExample`から`playExampleFile`にリネームされました。

## コアとなるコードの解説

### `src/cmd/godoc/godoc.go` の変更

```go
@@ -356,6 +356,7 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File
 		// (use tabs, no comment highlight, etc).
 		play := ""
 		if eg.Play != nil && *showPlayground {
+			ast.SortImports(fset, eg.Play)
 			var buf bytes.Buffer
 			err := (&printer.Config{Mode: printer.TabIndent, Tabwidth: 8}).Fprint(&buf, fset, eg.Play)
 			if err != nil {

この変更は、godocがExampleコードをHTMLとして表示し、Go Playgroundで実行可能な形式に変換する直前に行われます。ast.SortImports(fset, eg.Play)を呼び出すことで、ExampleコードのAST(eg.Play)内のインポート文が、Goの標準的なスタイルガイドラインに従ってソートされます。これにより、生成されるコードの見た目が整い、一貫性が保たれます。これは、gofmtgoimportsが自動的に行うインポートのソートと同じ効果をExampleコードにもたらします。

src/pkg/go/doc/example.go の変更

@@ -145,8 +145,9 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 	// Use unresolved identifiers to determine the imports used by this
 	// example. The heuristic assumes package names match base import
 	// paths. (Should be good enough most of the time.)
-	imports := make(map[string]string) // [name]path
-	for _, s := range file.Imports {
+	// paths for imports w/o renames (should be good enough most of the time).
+	namedImports := make(map[string]string) // [name]path
+	var blankImports []ast.Spec             // _ imports
+	for _, s := range file.Imports {
 		p, err := strconv.Unquote(s.Path.Value)
 		if err != nil {
 			continue
 		}
 		n := path.Base(p)
 		if s.Name != nil {
-			if s.Name.Name == "." {
+			n = s.Name.Name
+			switch n {
+			case "_":
+				blankImports = append(blankImports, s)
+				continue
+			case ".":
 				// We can't resolve dot imports (yet).
 				return nil
 			}
-			n = s.Name.Name
 		}
 		if unresolved[n] {
-			imports[n] = p
+			namedImports[n] = p
 			delete(unresolved, n)
 		}
 	}

この部分では、ExampleコードのASTからインポートを抽出するロジックが変更されています。

  • 以前はimportsという単一のマップで全てのインポートを管理していましたが、これをnamedImports(通常の名前付きインポート)とblankImports(ブランクインポート)の2つに分けました。
  • switch n文が追加され、インポートのName_(ブランクインポート)である場合に、そのast.ImportSpecblankImportsスライスに格納するようにしました。これにより、ブランクインポートが他のインポートとは異なる方法で処理され、確実に含まれるようになります。
@@ -172,13 +177,19 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 		return nil
 	}
 
-	// Filter out comments that are outside the function body.
+	// Include documentation belonging to blank imports.
 	var comments []*ast.CommentGroup
+	for _, s := range blankImports {
+		if c := s.(*ast.ImportSpec).Doc; c != nil {
+			comments = append(comments, c)
+		}
+	}
+
+	// Include comments that are inside the function body.
 	for _, c := range file.Comments {
-		if c.Pos() < body.Pos() || c.Pos() >= body.End() {
-			continue
+		if body.Pos() <= c.Pos() && c.End() <= body.End() {
+			comments = append(comments, c)
 		}
-		comments = append(comments, c)
 	}

このセクションでは、Exampleコードに含めるコメントの処理が改善されています。

  • ブランクインポートに付随するドキュメンテーションコメント(s.(*ast.ImportSpec).Doc)もcommentsスライスに追加されるようになりました。これにより、ブランクインポートの意図を説明するコメントが失われることがなくなります。
  • 関数本体内のコメントを収集するロジックも、より明確な条件(body.Pos() <= c.Pos() && c.End() <= body.End())で書き直されています。
@@ -190,13 +201,14 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 		Lparen: 1, // Need non-zero Lparen and Rparen so that printer
 		Rparen: 1, // treats this as a factored import.
 	}
-	for n, p := range imports {
+	for n, p := range namedImports {
 		s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
 		if path.Base(p) != n {
 			s.Name = ast.NewIdent(n)
 		}
 		importDecl.Specs = append(importDecl.Specs, s)
 	}
+	importDecl.Specs = append(importDecl.Specs, blankImports...)
 
 	// Synthesize main function.
 	funcDecl := &ast.FuncDecl{

この部分では、最終的なインポート宣言(importDecl.Specs)を構築する際に、namedImportsに加えてblankImportsスライスに含まれる全てのブランクインポートも追加されるようになりました。これにより、ブランクインポートが確実に生成されるExampleコードに含まれることが保証されます。

@@ -213,7 +225,7 @@ func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
 	}\n}\n\n-// playExample takes a whole file example and synthesizes a new *ast.File\n+// playExampleFile takes a whole file example and synthesizes a new *ast.File\n // such that the example is function main in package main.\n func playExampleFile(file *ast.File) *ast.File {\n 	// Strip copyright comment if present.\n```
関数名が`playExample`から`playExampleFile`にリネームされました。これは、この関数がファイル全体のExampleを処理するというその役割をより正確に反映するための変更です。

これらの変更は全体として、`go/doc`がExampleコードをより正確に解析し、Goの言語仕様とツールが期待する形式でインポートを処理できるようにすることで、ドキュメンテーションの品質とExampleの信頼性を向上させます。

## 関連リンク

*   GitHubコミットページ: [https://github.com/golang/go/commit/80dcc434a8a20ddc579810f88e770e098e7f9eb8](https://github.com/golang/go/commit/80dcc434a8a20ddc579810f88e770e098e7f9eb8)
*   Go Change List 6782082: [https://golang.org/cl/6782082](https://golang.org/cl/6782082) (このコミットに対応するCL)
*   Go Change List 6813061: [https://golang.org/cl/6813061](https://golang.org/cl/6813061) (このコミットが代替する以前のCL)

## 参考にした情報源リンク

*   Go言語のブランクインポートに関する情報: [https://pkg.go.dev/cmd/go#hdr-Blank_imports](https://pkg.go.dev/cmd/go#hdr-Blank_imports) (Go公式ドキュメント)
*   `go/ast`パッケージの`SortImports`関数に関する情報: [https://pkg.go.dev/go/ast#SortImports](https://pkg.go.dev/go/ast#SortImports) (Go公式ドキュメント)
*   `goimports`ツールに関する情報: [https://pkg.go.dev/golang.org/x/tools/cmd/goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) (Go公式ドキュメント)
*   Go Playground: [https://go.dev/play/](https://go.dev/play/)