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

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

このコミットは、Go言語のドキュメンテーションツールである godocsrc/cmd/godoc/godoc.go ファイルに対する変更です。主な目的は、Goのパッケージドキュメントに表示されるExampleコードの処理を改善することです。具体的には、命名規則に沿っていないExample(misnamed examples)を無視し、その際に警告メッセージを出力するように修正されています。これにより、godoc が生成するドキュメントの品質と正確性が向上します。

コミット

commit 267a55397b4343fe37ea89319b9007e649eb101f
Author: Kamil Kisiel <kamil@kamilkisiel.net>
Date:   Wed Jan 2 16:00:41 2013 +1100

    cmd/godoc: ignore misnamed examples and print a warning
    
    Fixes #4211.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/6970051

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

https://github.com/golang/go/commit/267a55397b4343fe37ea89319b9007e649eb101f

元コミット内容

cmd/godoc: ignore misnamed examples and print a warning

このコミットは、godoc コマンドが誤った名前のExampleを無視し、警告を出力するように修正します。

Fixes #4211.

Issue #4211を修正します。

変更の背景

Go言語の godoc ツールは、Goのソースコードから自動的にドキュメントを生成する際に、Exampleコードも抽出して表示します。Exampleコードは、関数や型の使用方法を示すための重要な要素であり、通常は Example<FunctionName>Example<TypeName>_<MethodName> のような特定の命名規則に従って _test.go ファイル内に記述されます。

しかし、これまでの godoc の実装では、これらの命名規則に厳密に従っていないExampleコードも、場合によってはドキュメントに表示されてしまう可能性がありました。これにより、生成されるドキュメントに不正確な情報が含まれたり、ユーザーがExampleコードとそれが関連する関数や型との関係を誤解したりする原因となっていました。

このコミットは、この問題を解決するために導入されました。具体的には、Exampleコードが実際にそのパッケージ内で宣言されている関数や型に関連付けられているかを検証し、関連付けられない「誤った名前のExample」をドキュメントから除外し、開発者に対して警告メッセージを出力することで、ドキュメントの正確性と信頼性を向上させることを目的としています。コミットメッセージに記載されている Fixes #4211 は、この問題がGoのIssueトラッカーで報告されていたことを示唆していますが、現在のところ、この特定のIssueに関する詳細な情報は公開されていません。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語および godoc 関連の概念を理解しておく必要があります。

  • godoc ツール: Go言語に標準で付属するドキュメンテーションツールです。Goのソースコードを解析し、パッケージ、関数、型、変数などのドキュメントを自動生成します。Webサーバーとして起動し、ブラウザでドキュメントを閲覧することも可能です。
  • Exampleコード: Goのパッケージドキュメントに表示される、関数や型の使用例を示すコードスニペットです。_test.go ファイル内に func Example<Name>() の形式で記述されます。godoc はこれらのExampleを自動的に抽出し、関連するドキュメントに表示します。
  • Exampleの命名規則: Example関数は、それが関連する関数、型、またはメソッドの名前と一致するように命名されます。
    • 関数 Foo のExample: func ExampleFoo()
    • Bar のExample: func ExampleBar()
    • Bar のメソッド Baz のExample: func ExampleBar_Baz()
    • 特定の出力を持つExample: func ExampleFoo_Output() のように、// Output: コメントを伴うことで、そのExampleの実行結果がドキュメントに表示されます。
  • go/ast パッケージ: Goのソースコードを抽象構文木(AST: Abstract Syntax Tree)として表現するためのデータ構造と関数を提供します。godoc はこのパッケージを使用してソースコードを解析します。
  • go/token パッケージ: ソースコード内の位置(ファイル、行、列)を管理するための型と関数を提供します。ASTノードがソースコードのどこに位置するかを特定するのに使用されます。
  • go/doc パッケージ: go/ast パッケージで解析されたASTから、Goのドキュメントを生成するための高レベルな機能を提供します。Exampleコードの抽出もこのパッケージの doc.Examples 関数によって行われます。
  • ast.Decl: 抽象構文木における宣言(関数宣言、型宣言、変数宣言など)を表すインターフェースです。
  • ast.FuncDecl: 関数宣言を表すASTノードです。
  • ast.GenDecl: import, const, type, var などの一般的な宣言を表すASTノードです。
  • ast.TypeSpec: 型宣言(type MyType int など)を表すASTノードです。
  • ast.ValueSpec: 変数宣言や定数宣言(var x int など)を表すASTノードです。
  • レシーバー(Receiver): Goのメソッドにおいて、メソッドが操作する型を指定する部分です。func (r MyType) MyMethod()(r MyType) がレシーバーです。

技術的詳細

このコミットは、godoc がExampleコードを処理するロジックを大幅に改善しています。主な変更点は以下の通りです。

  1. stripExampleSuffix 関数の導入:

    • 以前は example_htmlFunc 内にインラインで存在していた、Example名からサフィックスを削除するロジックが stripExampleSuffix という独立した関数として抽出されました。
    • この関数は、Foo_braz のような小文字で始まるサフィックス(_braz)を削除して Foo にしますが、Foo_Braz のような大文字で始まるサフィックス(_Braz)は保持します。これは、Exampleが特定の関数や型に関連付けられているかを正確に判断するために重要です。
  2. declNames 関数の導入:

    • ast.Decl(ASTの宣言ノード)から、その宣言が持つ名前(関数名、型名、変数名など)を抽出するヘルパー関数です。
    • 関数宣言 (ast.FuncDecl) の場合、レシーバーを持つメソッドであれば Receiver_Method の形式で名前を返します(例: MyType_MyMethod)。
    • 一般的な宣言 (ast.GenDecl) の場合、型宣言 (ast.TypeSpec) や値宣言 (ast.ValueSpec) から名前を抽出します。
  3. globalNames 関数の導入:

    • 与えられたASTパッケージのマップから、すべてのトップレベル宣言(グローバルな関数、型、変数)の名前を抽出し、それらをキーとする map[string]bool を返します。
    • このマップは、Exampleが参照している名前が、実際にそのパッケージ内で宣言されている有効な識別子であるかをチェックするために使用されます。
  4. parseExamples 関数の導入とExampleフィルタリングロジックの集約:

    • getPageInfo 関数内に分散していたExampleの解析ロジックが parseExamples という新しい関数に集約されました。
    • この関数は、指定されたディレクトリ内の _test.go ファイルを解析し、Exampleコードを抽出します。
    • 重要な変更点は、抽出された各Exampleに対して、そのExample名がパッケージ内の既存のグローバルな識別子(関数、型など)と一致するかどうかを検証するようになった点です。
    • stripExampleSuffix を使用してExampleの基本名を取得し、その基本名が globalNames で取得したマップ内に存在するかどうかを確認します。
    • もしExampleの基本名が空であるか、またはパッケージ内の既存の識別子と一致する場合のみ、そのExampleは有効とみなされ、ドキュメントに含められます。
    • それ以外の場合(つまり、Exampleがパッケージ内のどの識別子にも関連付けられない場合)、そのExampleは「誤った名前のExample」と判断され、log.Printf を使用して警告メッセージが出力され、ドキュメントからは除外されます。これにより、不正確なExampleがドキュメントに表示されるのを防ぎます。
  5. getPageInfo の変更:

    • Exampleを解析する既存のロジックが削除され、新しく導入された parseExamples 関数を呼び出すように変更されました。これにより、コードの重複が排除され、Example解析ロジックが一元化されました。

これらの変更により、godoc はExampleコードの命名規則をより厳密にチェックし、誤ったExampleを自動的にフィルタリングして警告を出すことで、生成されるドキュメントの品質と信頼性を大幅に向上させています。

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

変更は src/cmd/godoc/godoc.go ファイルに集中しています。

追加された関数:

  • stripExampleSuffix(name string) string (行 320-326)
  • declNames(decl ast.Decl) (names []string) (行 905-930)
  • globalNames(pkgs map[string]*ast.Package) map[string]bool (行 932-944)
  • parseExamples(fset *token.FileSet, pkgs map[string]*ast.Package, dir string) ([]*doc.Example, error) (行 946-972)

変更された関数:

  • example_htmlFunc (行 330-342): stripExampleSuffix の呼び出しに置き換え。

    --- a/src/cmd/godoc/godoc.go
    +++ b/src/cmd/godoc/godoc.go
    @@ -317,18 +317,21 @@ func startsWithUppercase(s string) bool {
     
     var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`)
     
    +// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
    +// while keeping uppercase Braz in Foo_Braz.
    +func stripExampleSuffix(name string) string {
    +	if i := strings.LastIndex(name, "_"); i != -1 {
    +		if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
    +			name = name[:i]
    +		}
    +	}
    +	return name
    +}
    +
     func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.FileSet) string {
     	var buf bytes.Buffer
     	for _, eg := range examples {
    -		name := eg.Name
    -
    -		// Strip lowercase braz in Foo_braz or Foo_Bar_braz from name
    -		// while keeping uppercase Braz in Foo_Braz.
    -		if i := strings.LastIndex(name, "_"); i != -1 {
    -			if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
    -				name = name[:i]
    -			}
    -		}
    +		name := stripExampleSuffix(eg.Name)
     
     		if name != funcName {
     			continue
    
  • getPageInfo (行 975-1060): Example解析ロジックが parseExamples の呼び出しに置き換え。

    --- a/src/cmd/godoc/godoc.go
    +++ b/src/cmd/godoc/godoc.go
    @@ -975,21 +1054,9 @@ func (h *docServer) getPageInfo(abspath, relpath string, mode PageInfoMode) Page
     		}
     	}
     
    -	// get examples from *_test.go files
    -	var examples []*doc.Example
    -	filter = func(d os.FileInfo) bool {
    -		return isGoFile(d) && strings.HasSuffix(d.Name(), "_test.go")
    -	}
    -	if testpkgs, err := parseDir(fset, abspath, filter); err != nil {
    -		log.Println("parsing test files:", err)
    -	} else {
    -		for _, testpkg := range testpkgs {
    -			var files []*ast.File
    -			for _, f := range testpkg.Files {
    -				files = append(files, f)
    -			}
    -			examples = append(examples, doc.Examples(files...)...)
    -		}
    +	examples, err := parseExamples(fset, pkgs, abspath)
    +	if err != nil {
    +		log.Println("parsing examples:", err)
     	}
     
      	// compute package documentation
    

コアとなるコードの解説

stripExampleSuffix 関数

func stripExampleSuffix(name string) string {
	if i := strings.LastIndex(name, "_"); i != -1 {
		if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
			name = name[:i]
		}
	}
	return name
}

この関数は、Example名からアンダースコア (_) で区切られたサフィックスを削除します。ただし、サフィックスの最初の文字が大文字の場合は削除しません。例えば、ExampleFoo_barExampleFoo になりますが、ExampleFoo_Bar はそのままです。これは、ExampleFoo_BarBar という型や関数に関連するExampleである可能性を考慮しているためです。

declNames 関数

func declNames(decl ast.Decl) (names []string) {
	switch d := decl.(type) {
	case *ast.FuncDecl:
		name := d.Name.Name
		if d.Recv != nil {
			var typeName string
			switch r := d.Recv.List[0].Type.(type) {
			case *ast.StarExpr:
				typeName = r.X.(*ast.Ident).Name
			case *ast.Ident:
				typeName = r.Name
			}
			name = typeName + "_" + name
		}
		names = []string{name}
	case *ast.GenDecl:
		for _, spec := range d.Specs {
			switch s := spec.(type) {
			case *ast.TypeSpec:
				names = append(names, s.Name.Name)
			case *ast.ValueSpec:
				for _, id := range s.Names {
					names = append(names, id.Name)
				}
			}
		}
	}
	return
}

この関数は、ASTの宣言ノード (ast.Decl) を受け取り、その宣言によって定義されるすべての識別子名を文字列スライスとして返します。

  • 関数宣言 (ast.FuncDecl): 関数名を取得します。もしレシーバーを持つメソッドであれば、レシーバー型名_メソッド名 の形式(例: MyStruct_MyMethod)で名前を生成します。
  • 一般的な宣言 (ast.GenDecl): typevar/const 宣言に含まれる型名や変数名/定数名を取得します。

globalNames 関数

func globalNames(pkgs map[string]*ast.Package) map[string]bool {
	names := make(map[string]bool)
	for _, pkg := range pkgs {
		for _, file := range pkg.Files {
			for _, decl := range file.Decls {
				for _, name := range declNames(decl) {
					names[name] = true
				}
			}
		}
	}
	return names
}

この関数は、与えられたパッケージのAST情報 (map[string]*ast.Package) を走査し、そのパッケージ内で宣言されているすべてのトップレベルの識別子名(関数、型、変数など)を収集します。結果は map[string]bool の形式で返され、キーが存在すればその名前がパッケージ内で宣言されていることを示します。これは、Exampleが参照している名前が実際に存在するかどうかを効率的にチェックするために使用されます。

parseExamples 関数

func parseExamples(fset *token.FileSet, pkgs map[string]*ast.Package, dir string) ([]*doc.Example, error) {
	var examples []*doc.Example
	filter := func(d os.FileInfo) bool {
		return isGoFile(d) && strings.HasSuffix(d.Name(), "_test.go")
	}
	testpkgs, err := parseDir(fset, dir, filter)
	if err != nil {
		return nil, err
	}
	globals := globalNames(pkgs) // パッケージ内のグローバル識別子を取得
	for _, testpkg := range testpkgs {
		var files []*ast.File
		for _, f := range testpkg.Files {
			files = append(files, f)
		}
		for _, e := range doc.Examples(files...) {
			name := stripExampleSuffix(e.Name) // Exampleの基本名を取得
			if name == "" || globals[name] { // 基本名が空か、グローバル識別子に存在するかチェック
				examples = append(examples, e)
			} else {
				log.Printf("skipping example Example%s: refers to unknown function or type", e.Name)
			}
		}
	}
	return examples, nil
}

この関数は、指定されたディレクトリ内の _test.go ファイルを解析し、Exampleコードを抽出します。

  1. まず、parseDir を使用してテストファイルをASTとして解析します。
  2. 次に、globalNames を呼び出して、現在のパッケージで宣言されているすべてのグローバル識別子のセットを取得します。
  3. 抽出された各Exampleについて、stripExampleSuffix を使ってそのExampleの基本名(例: ExampleFoo_bar から ExampleFoo)を取得します。
  4. 重要なフィルタリングロジック: 取得した基本名が空であるか、または globalNames で取得したグローバル識別子のセット内に存在する場合にのみ、そのExampleを結果に含めます。
  5. それ以外の場合(つまり、Exampleがパッケージ内の既存の関数や型に適切に関連付けられていない場合)、log.Printf を使用して警告メッセージを出力し、そのExampleはドキュメントから除外されます。

この parseExamples 関数が、誤った名前のExampleを識別し、無視する主要なメカニズムを提供します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびソースコード
  • Go言語のExampleに関する公式ガイドライン (Goのドキュメントやブログ記事)