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

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

このコミットは、Go言語のドキュメンテーションツールであるgodocと、ドキュメンテーション生成ライブラリであるgo/docにおける、Example関数の表示方法に関する改善です。具体的には、単一のExample関数しか含まないファイルが、そのファイル全体をExampleとして表示する「whole file example」として扱われるように変更されました。

コミット

commit e11632ee0044474d3e767192d6f61e6ab010c48d
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Feb 14 17:19:59 2012 +1100

    go/doc, godoc: regard lone examples as "whole file" examples
    
    Fixes #2930.
    
    R=r, gri, rsc
    CC=golang-dev
    https://golang.org/cl/5657048

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

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

元コミット内容

このコミットの目的は、「単一のExample関数しか含まないファイル」を「whole file example」として扱うことです。これにより、godocが生成するドキュメンテーションにおいて、Example関数だけでなく、そのExampleが依存する型、変数、定数などの宣言もExampleコードの一部として表示されるようになります。これは、GoのIssue #2930を解決するための変更です。

変更の背景

Go言語のドキュメンテーションツールgodocは、コード内のExample関数(ExampleFのような命名規則に従う関数)を抽出し、そのコードと出力をドキュメンテーションに含める機能を持っています。しかし、これまでの実装では、Example関数が定義されているファイル内に他の関数や型、変数などの宣言があっても、Example関数本体のコードブロックのみが抽出されて表示されていました。

これにより、Exampleが依存する補助的なコード(例えば、Example内で使用されるカスタム型やヘルパー関数)がドキュメンテーションに表示されず、Exampleが単独では理解しにくい、あるいは不完全に見えるという問題がありました。特に、Exampleがそのファイル内で定義された特定の型や関数に密接に結合している場合、Example関数本体だけを見ても、そのExampleが何を示しているのか、どのように動作するのかが不明瞭になることがありました。

このコミットは、このような問題を解決するために、特定の条件を満たすExample関数(ファイル内に単一のExample関数しかなく、かつ他のトップレベル宣言が存在し、テストやベンチマーク関数がない場合)に対して、Example関数本体だけでなく、ファイル全体をExampleコードとして表示するようにgodocの振る舞いを変更することを目的としています。これにより、Exampleの完全性と理解しやすさが向上します。

前提知識の解説

Go言語のExample関数

Go言語には、コードの利用例を示すための特別な関数である「Example関数」があります。これらはExampleというプレフィックスで始まり、ExamplePackageExampleTypeExampleFunctionExampleType_Methodといった命名規則に従います。Example関数は、go testコマンドで実行され、その出力が期待される出力と一致するかどうかを検証することができます。また、godocツールによって自動的に抽出され、生成されるドキュメンテーションにコードスニペットとして表示されます。

Example関数は、Goのドキュメンテーションにおいて非常に重要な役割を果たします。単なるAPIリファレンスだけでなく、実際にコードがどのように使われるかを示すことで、ライブラリやパッケージの理解を深める手助けとなります。

godocツールとgo/docパッケージ

  • godoc: Go言語のソースコードからドキュメンテーションを生成し、HTTPサーバーとして提供するツールです。開発者がローカルでGoの標準ライブラリや自身のプロジェクトのドキュメンテーションを閲覧する際に利用されます。godocは、コードコメント、関数シグネチャ、Example関数などを解析して、整形されたHTMLドキュメントを生成します。
  • go/docパッケージ: godocツールが内部的に利用するGoの標準ライブラリです。Goのソースコード(AST: Abstract Syntax Tree)を解析し、パッケージ、型、関数、Example関数などのドキュメンテーション情報を抽出するための機能を提供します。このパッケージが、Example関数をどのように解釈し、どの範囲のコードをExampleとして扱うかを決定する中心的な役割を担っています。

AST (Abstract Syntax Tree)

ASTは、プログラムのソースコードの抽象的な構文構造をツリー形式で表現したものです。コンパイラやリンター、ドキュメンテーションツールなどがソースコードを解析する際に利用します。Go言語のgo/astパッケージは、GoのソースコードをASTとしてパースする機能を提供します。このコミットでは、go/docパッケージがASTを操作して、Example関数のボディをファイル全体に置き換えることで、「whole file example」を実現しています。

技術的詳細

このコミットの核心は、src/pkg/go/doc/example.go内のExamples関数のロジック変更にあります。

変更前は、Examples関数は各ソースファイルからExampleプレフィックスを持つ関数を抽出し、その関数のボディ(f.Body)をExampleのコードとして扱っていました。

変更後は、以下の条件がすべて満たされた場合に、Example関数のボディをファイル全体(file ASTノード)に置き換えるロジックが追加されました。

  1. ファイル内にテスト関数やベンチマーク関数が存在しないこと (!hasTests): Example関数とテスト/ベンチマーク関数が混在しているファイルでは、この「whole file example」のロジックは適用されません。これは、テストやベンチマークのコードがExampleとして表示されることを避けるためです。
  2. ファイル内にインポート宣言以外のトップレベル宣言が複数存在すること (numDecl > 1): Example関数以外にも、型、変数、定数、または他の関数などの宣言がファイル内に存在する場合に適用されます。これにより、Exampleが単独で存在する場合(例えば、Example関数しか含まないファイル)は、これまで通りExample関数本体のみが表示され、Exampleが他の補助的な宣言に依存している場合にのみ「whole file example」が適用されるようになります。
  3. ファイル内に単一のExample関数のみが存在すること (len(flist) == 1): 複数のExample関数が存在するファイルでは、どのExampleを「whole file example」として扱うべきか不明確になるため、このロジックは適用されません。

これらの条件が満たされた場合、抽出された単一のExampleオブジェクトのBody.Nodeフィールドが、Example関数本体のASTノードから、そのExample関数が定義されているファイル全体のASTノードに置き換えられます。これにより、godocがExampleコードをレンダリングする際に、Example関数本体だけでなく、ファイル全体のコードが表示されるようになります。

また、src/cmd/godoc/godoc.goでは、HTML出力時にExampleコードの整形方法が調整されています。これまでの実装では、Exampleコードの先頭と末尾の波括弧を無条件に削除し、インデントを解除していました。この変更により、コードが波括弧で始まる場合にのみこれらの整形処理を行うようになり、「whole file example」のようにファイル全体がExampleとして扱われる場合に、不要な波括弧の削除やインデント解除が行われないように修正されました。

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

src/pkg/go/doc/example.go

--- a/src/pkg/go/doc/example.go
+++ b/src/pkg/go/doc/example.go
@@ -9,6 +9,7 @@ package doc
 import (
 	"go/ast"
 	"go/printer"
+	"go/token"
 	"strings"
 	"unicode"
 	"unicode/utf8"
@@ -21,28 +22,47 @@ type Example struct {
 }
 
 func Examples(pkg *ast.Package) []*Example {
-	var examples []*Example
-	for _, src := range pkg.Files {
-		for _, decl := range src.Decls {
+	var list []*Example
+	for _, file := range pkg.Files {
+		hasTests := false // file contains tests or benchmarks
+		numDecl := 0      // number of non-import declarations in the file
+		var flist []*Example
+		for _, decl := range file.Decls {
+			if g, ok := decl.(*ast.GenDecl); ok && g.Tok != token.IMPORT {
+				numDecl++
+				continue
+			}
 			f, ok := decl.(*ast.FuncDecl)
 			if !ok {
 				continue
 			}
+			numDecl++
 			name := f.Name.Name
+			if isTest(name, "Test") || isTest(name, "Benchmark") {
+				hasTests = true
+				continue
+			}
 			if !isTest(name, "Example") {
 				continue
 			}
-			examples = append(examples, &Example{
+			flist = append(flist, &Example{
 				Name: name[len("Example"):],
 				Body: &printer.CommentedNode{
 					Node:     f.Body,
-					Comments: src.Comments,
+					Comments: file.Comments,
 				},
 				Output: f.Doc.Text(),
 			})
 		}
+		if !hasTests && numDecl > 1 && len(flist) == 1 {
+			// If this file only has one example function, some
+			// other top-level declarations, and no tests or
+			// benchmarks, use the whole file as the example.
+			flist[0].Body.Node = file
+		}
+		list = append(list, flist...)
 	}
-	return examples
+	return list
 }
 
 // isTest tells whether name looks like a test, example, or benchmark.

src/cmd/godoc/godoc.go

--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -516,10 +516,13 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File
 			continue
 		}
 
-		// print code, unindent and remove surrounding braces
+		// print code
 		code := node_htmlFunc(eg.Body, fset)
-		code = strings.Replace(code, "\n    ", "\n", -1)
-		code = code[2 : len(code)-2]
+		if len(code) > 0 && code[0] == '{' {
+			// unindent and remove surrounding braces
+			code = strings.Replace(code, "\n    ", "\n", -1)
+			code = code[2 : len(code)-2]
+		}
 
 		err := exampleHTML.Execute(&buf, struct {
 			Name, Code, Output string

コアとなるコードの解説

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

  • tokenパッケージのインポート: token.IMPORT定数を使用するためにgo/tokenパッケージがインポートされました。
  • ファイルごとの処理の導入: Examples関数は、パッケージ内の各ファイル(pkg.Files)を個別にループ処理するようになりました。
  • hasTestsnumDeclの追跡:
    • hasTestsは、現在のファイルにテスト関数(Testプレフィックス)またはベンチマーク関数(Benchmarkプレフィックス)が含まれているかどうかを示すフラグです。
    • numDeclは、現在のファイル内のインポート宣言以外のトップレベル宣言(関数、型、変数、定数)の数をカウントします。
  • Example関数の収集と条件付き「whole file」化:
    • 各ファイル内で見つかったExample関数は一時的なリストflistに収集されます。
    • Example関数が見つかるたびにnumDeclもインクリメントされます。
    • テスト関数やベンチマーク関数が見つかった場合、hasTeststrueに設定されます。
    • ループの最後に、以下の条件がチェックされます。
      • !hasTests: ファイルにテスト/ベンチマーク関数がない。
      • numDecl > 1: インポート以外のトップレベル宣言が複数ある。
      • len(flist) == 1: Example関数がちょうど1つだけある。
    • これらの条件がすべて満たされた場合、flist内の唯一のExampleオブジェクトのBody.Nodeが、Example関数本体のASTノードから、ファイル全体のASTノード(file)に置き換えられます。これが「whole file example」を実現する核心部分です。
  • 最終的なExampleリストの構築: 各ファイルから抽出されたExample(「whole file example」を含む)が、最終的なlistに結合されて返されます。

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

  • 条件付きコード整形: example_htmlFunc関数内で、ExampleコードをHTMLとして表示する際の整形ロジックが変更されました。
  • 変更前は、Exampleコードの先頭と末尾の波括弧を無条件に削除し、インデントを解除していました。
  • 変更後は、if len(code) > 0 && code[0] == '{'という条件が追加されました。これにより、Exampleコードが波括弧で始まる場合にのみ、波括弧の削除とインデント解除が行われるようになりました。
  • この変更は、「whole file example」が導入されたことによるものです。「whole file example」の場合、Exampleのボディはファイル全体になるため、先頭に波括弧がない可能性があります。無条件に波括弧を削除しようとすると、予期せぬ結果になるため、この条件付きの整形が必要となりました。

関連リンク

参考にした情報源リンク