[インデックス 14768] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールである godoc
の src/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コードを処理するロジックを大幅に改善しています。主な変更点は以下の通りです。
-
stripExampleSuffix
関数の導入:- 以前は
example_htmlFunc
内にインラインで存在していた、Example名からサフィックスを削除するロジックがstripExampleSuffix
という独立した関数として抽出されました。 - この関数は、
Foo_braz
のような小文字で始まるサフィックス(_braz
)を削除してFoo
にしますが、Foo_Braz
のような大文字で始まるサフィックス(_Braz
)は保持します。これは、Exampleが特定の関数や型に関連付けられているかを正確に判断するために重要です。
- 以前は
-
declNames
関数の導入:ast.Decl
(ASTの宣言ノード)から、その宣言が持つ名前(関数名、型名、変数名など)を抽出するヘルパー関数です。- 関数宣言 (
ast.FuncDecl
) の場合、レシーバーを持つメソッドであればReceiver_Method
の形式で名前を返します(例:MyType_MyMethod
)。 - 一般的な宣言 (
ast.GenDecl
) の場合、型宣言 (ast.TypeSpec
) や値宣言 (ast.ValueSpec
) から名前を抽出します。
-
globalNames
関数の導入:- 与えられたASTパッケージのマップから、すべてのトップレベル宣言(グローバルな関数、型、変数)の名前を抽出し、それらをキーとする
map[string]bool
を返します。 - このマップは、Exampleが参照している名前が、実際にそのパッケージ内で宣言されている有効な識別子であるかをチェックするために使用されます。
- 与えられたASTパッケージのマップから、すべてのトップレベル宣言(グローバルな関数、型、変数)の名前を抽出し、それらをキーとする
-
parseExamples
関数の導入とExampleフィルタリングロジックの集約:getPageInfo
関数内に分散していたExampleの解析ロジックがparseExamples
という新しい関数に集約されました。- この関数は、指定されたディレクトリ内の
_test.go
ファイルを解析し、Exampleコードを抽出します。 - 重要な変更点は、抽出された各Exampleに対して、そのExample名がパッケージ内の既存のグローバルな識別子(関数、型など)と一致するかどうかを検証するようになった点です。
stripExampleSuffix
を使用してExampleの基本名を取得し、その基本名がglobalNames
で取得したマップ内に存在するかどうかを確認します。- もしExampleの基本名が空であるか、またはパッケージ内の既存の識別子と一致する場合のみ、そのExampleは有効とみなされ、ドキュメントに含められます。
- それ以外の場合(つまり、Exampleがパッケージ内のどの識別子にも関連付けられない場合)、そのExampleは「誤った名前のExample」と判断され、
log.Printf
を使用して警告メッセージが出力され、ドキュメントからは除外されます。これにより、不正確なExampleがドキュメントに表示されるのを防ぎます。
-
getPageInfo
の変更:- Exampleを解析する既存のロジックが削除され、新しく導入された
parseExamples
関数を呼び出すように変更されました。これにより、コードの重複が排除され、Example解析ロジックが一元化されました。
- 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_bar
は ExampleFoo
になりますが、ExampleFoo_Bar
はそのままです。これは、ExampleFoo_Bar
が Bar
という型や関数に関連する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
):type
やvar
/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コードを抽出します。
- まず、
parseDir
を使用してテストファイルをASTとして解析します。 - 次に、
globalNames
を呼び出して、現在のパッケージで宣言されているすべてのグローバル識別子のセットを取得します。 - 抽出された各Exampleについて、
stripExampleSuffix
を使ってそのExampleの基本名(例:ExampleFoo_bar
からExampleFoo
)を取得します。 - 重要なフィルタリングロジック: 取得した基本名が空であるか、または
globalNames
で取得したグローバル識別子のセット内に存在する場合にのみ、そのExampleを結果に含めます。 - それ以外の場合(つまり、Exampleがパッケージ内の既存の関数や型に適切に関連付けられていない場合)、
log.Printf
を使用して警告メッセージを出力し、そのExampleはドキュメントから除外されます。
この parseExamples
関数が、誤った名前のExampleを識別し、無視する主要なメカニズムを提供します。
関連リンク
- Go言語公式ドキュメント: https://go.dev/
godoc
コマンドのドキュメント: https://pkg.go.dev/cmd/godocgo/doc
パッケージのドキュメント: https://pkg.go.dev/go/docgo/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast
参考にした情報源リンク
- Go言語の公式ドキュメントおよびソースコード
- Go言語のExampleに関する公式ガイドライン (Goのドキュメントやブログ記事)