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

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

このコミットは、Go言語のドキュメント生成ツールであるgo/docパッケージの重要な改善を含んでいます。具体的には、匿名(埋め込み)の非エクスポートフィールドが持つエクスポートされたメソッドが、生成されるドキュメントから無視される問題を修正します。この修正に伴い、以前この問題の回避策として存在していたtestingパッケージ内のwrapper.goファイルが削除され、関連するコードもクリーンアップされました。

コミット

go/doc: don't ignore anonymous non-exported fields

- remove wrapper.go from testing package (not needed anymore)

Fixes #1000.

R=rsc, golang-dev, n13m3y3r
CC=golang-dev
https://golang.org/cl/5502074

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

https://github.com/golang/go/commit/9535b86a27bb6ef585e6bafe89ba19dd1bff2cb7

元コミット内容

go/doc: don't ignore anonymous non-exported fields

- remove wrapper.go from testing package (not needed anymore)

Fixes #1000.

R=rsc, golang-dev, n13m3y3r
CC=golang-dev
https://golang.org/cl/5502074

変更の背景

Go言語のドキュメント生成ツールであるgodoc(内部でgo/docパッケージを使用)は、Goのソースコードから自動的にドキュメントを生成し、開発者がAPIを理解する上で不可欠な役割を果たしています。しかし、以前のgo/docの実装には、構造体内に匿名で埋め込まれた非エクスポートフィールドが持つエクスポートされたメソッドが、生成されるドキュメントに含まれないという既知のバグが存在していました。

Goの埋め込みの性質上、埋め込まれた型のメソッドは、埋め込み先の型から直接呼び出すことができます。したがって、たとえ埋め込まれたフィールド自体が非エクスポートであっても、そのフィールドが提供するエクスポートされたメソッドは、埋め込み先の型の公開APIの一部と見なされるべきであり、ドキュメントに表示されるべきでした。

この問題は、Go Issue #1000として追跡されており、src/pkg/testing/wrapper.goファイル内のコメント(// TODO: delete when godoc shows exported methods for unexported embedded fields.)からも、この問題が認識されており、一時的な回避策が講じられていたことが伺えます。このコミットは、根本的な原因であるgo/docのバグを修正し、その結果として不要になった回避策のコードを削除することを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念と関連パッケージの知識が役立ちます。

  • Goの埋め込み (Embedding in Go): Go言語の構造体は、他の構造体やインターフェースを匿名フィールドとして含めることができます。これを「埋め込み」と呼びます。埋め込まれた型のフィールドやメソッドは、あたかも外側の構造体自身のフィールドやメソッドであるかのように、直接アクセスできるようになります。これは、Goにおける「コンポジション(合成)」によるコードの再利用の強力なメカニズムであり、従来のオブジェクト指向言語の「継承」とは異なるアプローチを提供します。

    type Logger struct {
        prefix string
    }
    
    func (l Logger) Log(message string) {
        fmt.Printf("%s: %s\n", l.prefix, message)
    }
    
    type Server struct {
        Logger // Logger型を匿名で埋め込み
        Addr string
    }
    
    func main() {
        s := Server{Logger: Logger{prefix: "SERVER"}, Addr: ":8080"}
        s.Log("Server starting...") // ServerからLoggerのLogメソッドを直接呼び出し
    }
    
  • エクスポートされた識別子と非エクスポートされた識別子 (Exported and Unexported Identifiers): Go言語では、識別子(変数名、関数名、型名、構造体のフィールド名など)の最初の文字が大文字で始まる場合、その識別子はパッケージ外からアクセス可能(「エクスポートされる」)になります。一方、小文字で始まる場合は、その識別子は宣言されたパッケージ内でのみアクセス可能(「非エクスポート」)です。godocは通常、エクスポートされた識別子のみをドキュメント化の対象とします。

  • go/docパッケージ: Goの標準ライブラリの一部であり、Goのソースコードを解析してドキュメントを生成するためのAPIを提供します。godocコマンドは、このパッケージの機能を利用して、Goのパッケージ、型、関数、変数などのドキュメントを生成します。このパッケージは、Goのソースコードの抽象構文木(AST)を操作してドキュメント情報を抽出します。

  • go/astパッケージ (Abstract Syntax Tree): Goのソースコードを抽象構文木(AST)として表現するためのパッケージです。Goのコンパイラやツール(go/docを含む)は、ソースコードをこのASTに変換し、それを解析・操作することで様々な処理を行います。このコミットでは、ASTのノード(ast.FieldList, ast.Expr, ast.TypeSpecなど)を走査し、その構造を分析してドキュメント情報を抽出するロジックが変更されています。

  • go/tokenパッケージ: Goのソースコードの字句要素(トークン、例: func, var, identなど)や、ソースコード内の位置情報(ファイル、行、列)を扱うためのパッケージです。

  • runtime.Caller関数: runtimeパッケージに含まれる関数で、現在のゴルーチンのコールスタックに関する情報を取得するために使用されます。runtime.Caller(skip)は、skipで指定されたフレーム数だけ遡った呼び出し元のファイル名、行番号、関数名などを返します。このコミットでは、wrapper.goの削除によりコールスタックの深さが変わったため、この関数の引数が調整されています。

技術的詳細

このコミットの主要な目的は、go/docが匿名(埋め込み)の非エクスポートフィールドを正しく処理し、それらが持つエクスポートされたメソッドをドキュメントに含めるようにすることです。この修正は、主にsrc/pkg/go/doc/exports.gosrc/pkg/go/doc/reader.goの2つのファイルにわたる変更によって実現されています。

  1. go/docパッケージの変更点:

    • src/pkg/go/doc/doc.go: NewPackageDoc関数がdocReaderinitメソッドを呼び出す際に、exportsOnlyというブール値の引数を追加しました。これにより、docReaderがドキュメント生成時にエクスポートされた要素のみを対象とするか、あるいは全ての要素を対象とするかを制御できるようになります。これは、内部的なドキュメント生成ロジックの柔軟性を高めるための変更です。

    • src/pkg/go/doc/exports.go:

      • filterFieldList関数のシグネチャがfunc (doc *docReader) filterFieldList(tinfo *typeInfo, fields *ast.FieldList) (removedFields bool)に変更されました。tinfo *typeInfo引数の追加により、この関数が処理しているフィールドがどの型(typeInfo)に属しているかというコンテキストを得られるようになりました。これにより、匿名フィールドの処理において、そのフィールドが埋め込まれている親の型に関する情報を利用できるようになります。
      • 匿名フィールドの処理ロジックが大幅に修正されました。以前は、匿名フィールドがエクスポートされている場合にのみkeepField = trueとして、そのフィールドをドキュメントに含めていました。しかし、変更後は、匿名フィールドが非エクスポートであっても、その型が持つメソッドを収集するために、tinfo.addEmbeddedType(embedded, ptr)を呼び出して、埋め込み型としてtypeInfoに登録するようになりました。この変更は、非エクスポートの埋め込み型が持つエクスポートされたメソッドもドキュメントに反映させるための核心的な部分です。
      • filterType関数のシグネチャもfunc (doc *docReader) filterType(tinfo *typeInfo, typ ast.Expr) boolに変更されました。これにより、型をフィルタリングする際に、その型が属するtypeInfoを利用できるようになり、特にast.StructTypeast.InterfaceTypeのフィールドを処理する際に、より正確な情報に基づいてフィルタリングが行えるようになりました。
    • src/pkg/go/doc/reader.go:

      • typeInfo構造体にname stringisStruct boolフィールドが追加されました。nameは基本型名、isStructはその型が構造体であるかを示し、ドキュメント生成時の内部処理で利用されます。
      • docReader構造体にexportsOnly boolフィールドが追加され、init関数で初期化されるようになりました。これは、ドキュメント生成時にエクスポートされた要素のみを対象とするかどうかを制御します。
      • addFunc関数(メソッドを追加するロジック)が修正されました。レシーバの型が非エクスポートであっても、その型が埋め込み型として既にdoc.typesに認識されている場合は、そのメソッドを収集するようになりました。これにより、非エクスポートの埋め込み型に属するエクスポートされたメソッドも正しくドキュメントに反映されます。
      • makeTypeDocs関数(型ドキュメントを生成するロジック)が大幅に修正されました。特に、old.exported() || !doc.exportsOnlyという条件が追加され、非エクスポートの型であっても、それが埋め込み型である場合や、exportsOnlyfalse(つまり全ての型をドキュメント化するモード)である場合には、その型をドキュメントリストに追加するようになりました。これにより、非エクスポートの埋め込み型が持つメソッドもドキュメントに含めることができるようになりました。
      • collectEmbeddedMethods関数が修正され、埋め込み型がポインタ型である場合のレシーバ型の計算ロジックが改善されました。embeddedIsPtrという引数が追加され、深くネストされた埋め込み型の場合でも、レシーバ型が正しく解決されるようになりました。
  2. testingパッケージの変更点:

    • src/pkg/testing/wrapper.goの削除: このファイルは、go/docが非エクスポートの埋め込みフィールドのメソッドを正しく表示しない問題に対する一時的な回避策として存在していました。go/docの修正により、このファイルは不要になったため、完全に削除されました。この削除は、コードベースのクリーンアップと、一時的な回避策の解消を意味します。

    • src/pkg/testing/Makefileの変更: wrapper.goが削除されたため、Makefileからwrapper.goへの参照が削除されました。

    • src/pkg/testing/testing.goの変更: decorate関数内で使用されているruntime.Callerの引数が4から3に変更されました。runtime.Callerはコールスタックの深さを指定して呼び出し元情報を取得する関数であり、wrapper.goが削除されたことでコールスタックの深さが1つ減ったため、ログ出力時に正しいファイル名と行番号を取得するためにこの調整が必要となりました。

これらの変更により、go/docはGo言語の埋め込みのセマンティクスをより正確に反映し、非エクスポートの匿名フィールドが持つエクスポートされたメソッドも適切にドキュメントに含めることができるようになりました。

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

diff --git a/src/pkg/go/doc/doc.go b/src/pkg/go/doc/doc.go
index 044e996a9e..4011c1fc7a 100644
--- a/src/pkg/go/doc/doc.go
+++ b/src/pkg/go/doc/doc.go
@@ -56,7 +56,7 @@ type FuncDoc struct {
 // included in the documentation.
 func NewPackageDoc(pkg *ast.Package, importpath string, exportsOnly bool) *PackageDoc {
 	var r docReader
-	r.init(pkg.Name)
+	r.init(pkg.Name, exportsOnly)
 	filenames := make([]string, len(pkg.Files))
 	i := 0
 	for filename, f := range pkg.Files {
diff --git a/src/pkg/go/doc/exports.go b/src/pkg/go/doc/exports.go
index 9cd186a9c7..994bf503b5 100644
--- a/src/pkg/go/doc/exports.go
+++ b/src/pkg/go/doc/exports.go
@@ -33,7 +33,7 @@ func baseName(x ast.Expr) *ast.Ident {
 	return nil
 }
 
-func (doc *docReader) filterFieldList(fields *ast.FieldList) (removedFields bool) {
+func (doc *docReader) filterFieldList(tinfo *typeInfo, fields *ast.FieldList) (removedFields bool) {
 	if fields == nil {
 		return false
 	}
@@ -44,7 +44,18 @@ func (doc *docReader) filterFieldList(fields *ast.FieldList) (removedFields bool
 		if len(f.Names) == 0 {
 			// anonymous field
 			name := baseName(f.Type)
-\t\t\tkeepField = name != nil && name.IsExported()
+\t\t\tif name != nil && name.IsExported() {
+\t\t\t\t// we keep the field - in this case doc.addDecl
+\t\t\t\t// will take care of adding the embedded type
+\t\t\t\tkeepField = true
+\t\t\t} else if tinfo != nil {
+\t\t\t\t// we don't keep the field - add it as an embedded
+\t\t\t\t// type so we won't loose its methods, if any
+\t\t\t\tif embedded := doc.lookupTypeInfo(name.Name); embedded != nil {
+\t\t\t\t\t_, ptr := f.Type.(*ast.StarExpr)
+\t\t\t\t\ttinfo.addEmbeddedType(embedded, ptr)
+\t\t\t\t}
+\t\t\t}
 		} else {
 			n := len(f.Names)
 			f.Names = filterIdentList(f.Names)
@@ -54,7 +65,7 @@ func (doc *docReader) filterFieldList(fields *ast.FieldList) (removedFields bool
 			keepField = len(f.Names) > 0
 		}
 		if keepField {
-\t\t\tdoc.filterType(f.Type)
+\t\t\tdoc.filterType(nil, f.Type)
 			list[j] = f
 			j++
 		}
@@ -72,23 +83,23 @@ func (doc *docReader) filterParamList(fields *ast.FieldList) bool {
 	}
 	var b bool
 	for _, f := range fields.List {
-\t\tif doc.filterType(f.Type) {\n+\t\tif doc.filterType(nil, f.Type) {\
 			b = true
 		}
 	}
 	return b
 }
 
-func (doc *docReader) filterType(typ ast.Expr) bool {
+func (doc *docReader) filterType(tinfo *typeInfo, typ ast.Expr) bool {
 	switch t := typ.(type) {
 	case *ast.Ident:
 		return ast.IsExported(t.Name)
 	case *ast.ParenExpr:
-\t\treturn doc.filterType(t.X)
+\t\treturn doc.filterType(nil, t.X)
 	case *ast.ArrayType:
-\t\treturn doc.filterType(t.Elt)
+\t\treturn doc.filterType(nil, t.Elt)
 	case *ast.StructType:
-\t\tif doc.filterFieldList(t.Fields) {
+\t\tif doc.filterFieldList(tinfo, t.Fields) {
 			t.Incomplete = true
 		}
 		return len(t.Fields.List) > 0
@@ -97,16 +108,16 @@ func (doc *docReader) filterType(typ ast.Expr) bool {
 		b2 := doc.filterParamList(t.Results)
 		return b1 || b2
 	case *ast.InterfaceType:
-\t\tif doc.filterFieldList(t.Methods) {
+\t\tif doc.filterFieldList(tinfo, t.Methods) {
 			t.Incomplete = true
 		}
 		return len(t.Methods.List) > 0
 	case *ast.MapType:
-\t\tb1 := doc.filterType(t.Key)
-\t\tb2 := doc.filterType(t.Value)
+\t\tb1 := doc.filterType(nil, t.Key)
+\t\tb2 := doc.filterType(nil, t.Value)
 		return b1 || b2
 	case *ast.ChanType:
-\t\treturn doc.filterType(t.Value)
+\t\treturn doc.filterType(nil, t.Value)
 	}
 	return false
 }
@@ -116,12 +127,12 @@ func (doc *docReader) filterSpec(spec ast.Spec) bool {
 	case *ast.ValueSpec:
 		s.Names = filterIdentList(s.Names)
 		if len(s.Names) > 0 {
-\t\t\tdoc.filterType(s.Type)
+\t\t\tdoc.filterType(nil, s.Type)
 			return true
 		}
 	case *ast.TypeSpec:
 		if ast.IsExported(s.Name.Name) {
-\t\t\tdoc.filterType(s.Type)
+\t\t\tdoc.filterType(doc.lookupTypeInfo(s.Name.Name), s.Type)
 			return true
 		}
 	}
diff --git a/src/pkg/go/doc/reader.go b/src/pkg/go/doc/reader.go
index 86448d044e..025fc85a10 100644
--- a/src/pkg/go/doc/reader.go
+++ b/src/pkg/go/doc/reader.go
@@ -23,6 +23,8 @@ type embeddedType struct {
 }
 
 type typeInfo struct {
+\tname     string // base type name
+\tisStruct bool
 	// len(decl.Specs) == 1, and the element type is *ast.TypeSpec
 	// if the type declaration hasn't been seen yet, decl is nil
 	decl     *ast.GenDecl
@@ -35,6 +37,10 @@ type typeInfo struct {
 	methods   map[string]*ast.FuncDecl
 }
 
+func (info *typeInfo) exported() bool {\n+\treturn ast.IsExported(info.name)\n+}\n+\n func (info *typeInfo) addEmbeddedType(embedded *typeInfo, isPtr bool) {\
 	info.embedded = append(info.embedded, embeddedType{embedded, isPtr})\
 }
@@ -47,17 +53,19 @@ func (info *typeInfo) addEmbeddedType(embedded *typeInfo, isPtr bool) {\
 // printing the corresponding AST node).\
 //\
 type docReader struct {\
-\tdoc      *ast.CommentGroup // package documentation, if any\
-\tpkgName  string\
-\tvalues   []*ast.GenDecl // consts and vars\
-\ttypes    map[string]*typeInfo\
-\tembedded map[string]*typeInfo // embedded types, possibly not exported\
-\tfuncs    map[string]*ast.FuncDecl\
-\tbugs     []*ast.CommentGroup\
+\tdoc         *ast.CommentGroup // package documentation, if any\
+\tpkgName     string\
+\texportsOnly bool\
+\tvalues      []*ast.GenDecl // consts and vars\
+\ttypes       map[string]*typeInfo\
+\tembedded    map[string]*typeInfo // embedded types, possibly not exported\
+\tfuncs       map[string]*ast.FuncDecl\
+\tbugs        []*ast.CommentGroup\
 }\
 
-func (doc *docReader) init(pkgName string) {\
+func (doc *docReader) init(pkgName string, exportsOnly bool) {\
 \tdoc.pkgName = pkgName\
+\tdoc.exportsOnly = exportsOnly\
 \tdoc.types = make(map[string]*typeInfo)\
 \tdoc.embedded = make(map[string]*typeInfo)\
 \tdoc.funcs = make(map[string]*ast.FuncDecl)\
@@ -86,6 +94,7 @@ func (doc *docReader) lookupTypeInfo(name string) *typeInfo {\
 	}\
 	// type wasn't found - add one without declaration\
 	info := &typeInfo{\
+\t\tname:      name,\
 \t\tfactories: make(map[string]*ast.FuncDecl),\
 \t\tmethods:   make(map[string]*ast.FuncDecl),\
 \t}\
@@ -182,9 +191,23 @@ func (doc *docReader) addFunc(fun *ast.FuncDecl) {\
 	// determine if it should be associated with a type\
 	if fun.Recv != nil {\
 		// method\
-\t\ttyp := doc.lookupTypeInfo(baseTypeName(fun.Recv.List[0].Type, false))\
+\t\trecvTypeName := baseTypeName(fun.Recv.List[0].Type, true /* exported or not */ )\
+\t\tvar typ *typeInfo\
+\t\tif ast.IsExported(recvTypeName) {\
+\t\t\t// exported recv type: if not found, add it to doc.types\
+\t\t\ttyp = doc.lookupTypeInfo(recvTypeName)\
+\t\t} else {\n+\t\t\t// unexported recv type: if not found, do not add it\n+\t\t\t// (unexported embedded types are added before this\n+\t\t\t// phase, so if the type doesn\'t exist yet, we don\'t\n+\t\t\t// care about this method)\n+\t\t\ttyp = doc.types[recvTypeName]\
+\t\t}\
 		if typ != nil {\
 			// exported receiver type\
+\t\t\t// associate method with the type\n+\t\t\t// (if the type is not exported, it may be embedded\n+\t\t\t// somewhere so we need to collect the method anyway)\
 			setFunc(typ.methods, fun)\
 		}\
 		// otherwise don't show the method\
@@ -256,6 +279,7 @@ func (doc *docReader) addDecl(decl ast.Decl) {\
 					switch typ := spec.(*ast.TypeSpec).Type.(type) {\
 					case *ast.StructType:\
 						fields = typ.Fields\
+\t\t\t\t\t\tinfo.isStruct = true\
 					case *ast.InterfaceType:\
 						fields = typ.Methods\
 					}\
@@ -439,21 +463,25 @@ func (doc *docReader) makeTypeDocs(m map[string]*typeInfo) []*TypeDoc {\
 	list := make([]*TypeDoc, len(m))\
 	i := 0\
 	for _, old := range m {\
-\t\t// all typeInfos should have a declaration associated with\
-\t\t// them after processing an entire package - be conservative\
-\t\t// and check\
-\t\tif decl := old.decl; decl != nil {\
-\t\t\ttypespec := decl.Specs[0].(*ast.TypeSpec)\
+\t\t// old typeInfos may not have a declaration associated with them\
+\t\t// if they are not exported but embedded, or because the package\
+\t\t// is incomplete.\
+\t\tif decl := old.decl; decl != nil || !old.exported() {\
+\t\t\t// process the type even if not exported so that we have\n+\t\t\t// its methods in case they are embedded somewhere\
 \t\t\tt := new(TypeDoc)\
-\t\t\tdoc := typespec.Doc\
-\t\t\ttypespec.Doc = nil // doc consumed - remove from ast.TypeSpec node\
-\t\t\tif doc == nil {\
-\t\t\t\t// no doc associated with the spec, use the declaration doc, if any\
-\t\t\t\tdoc = decl.Doc\
+\t\t\tif decl != nil {\
+\t\t\t\ttypespec := decl.Specs[0].(*ast.TypeSpec)\
+\t\t\t\tdoc := typespec.Doc\
+\t\t\t\ttypespec.Doc = nil // doc consumed - remove from ast.TypeSpec node\
+\t\t\t\tif doc == nil {\
+\t\t\t\t\t// no doc associated with the spec, use the declaration doc, if any\
+\t\t\t\t\tdoc = decl.Doc\
+\t\t\t\t}\n+\t\t\t\tdecl.Doc = nil // doc consumed - remove from ast.Decl node\
+\t\t\t\tt.Doc = doc.Text()\
+\t\t\t\tt.Type = typespec\
 \t\t\t}\
-\t\t\tdecl.Doc = nil // doc consumed - remove from ast.Decl node\
-\t\t\tt.Doc = doc.Text()\
-\t\t\tt.Type = typespec\
 \t\t\tt.Consts = makeValueDocs(old.values, token.CONST)\
 \t\t\tt.Vars = makeValueDocs(old.values, token.VAR)\
 \t\t\tt.Factories = makeFuncDocs(old.factories)\
@@ -466,8 +504,12 @@ func (doc *docReader) makeTypeDocs(m map[string]*typeInfo) []*TypeDoc {\
 \t\t\tt.Decl = old.decl\
 \t\t\tt.order = i\
 \t\t\told.forward = t // old has been processed\
-\t\t\tlist[i] = t\
-\t\t\ti++\
+\t\t\t// only add the type to the final type list if it\n+\t\t\t// is exported or if we want to see all types\n+\t\t\tif old.exported() || !doc.exportsOnly {\n+\t\t\t\tlist[i] = t\n+\t\t\t\ti++\n+\t\t\t}\
 \t\t} else {\
 \t\t\t// no corresponding type declaration found - move any associated\
 \t\t\t// values, factory functions, and methods back to the top-level\
@@ -497,11 +539,10 @@ func (doc *docReader) makeTypeDocs(m map[string]*typeInfo) []*TypeDoc {\
 \t\t\t// old has been processed into t; collect embedded\
 \t\t\t// methods for t from the list of processed embedded\
 \t\t\t// types in old (and thus for which the methods are known)\
-\t\t\ttyp := t.Type\
-\t\t\tif _, ok := typ.Type.(*ast.StructType); ok {\
+\t\t\tif old.isStruct {\
 \t\t\t\t// struct\
 \t\t\t\tt.embedded = make(methodSet)\
-\t\t\t\tcollectEmbeddedMethods(t.embedded, old, typ.Name.Name)\
+\t\t\t\tcollectEmbeddedMethods(t.embedded, old, old.name, false)\
 \t\t\t} else {\
 \t\t\t\t// interface\
 \t\t\t\t// TODO(gri) fix this\
@@ -541,13 +582,19 @@ func (doc *docReader) makeTypeDocs(m map[string]*typeInfo) []*TypeDoc {\
 // deeply nested embedded methods with conflicting names are\
 // excluded.\
 //\
-func collectEmbeddedMethods(mset methodSet, info *typeInfo, recvTypeName string) {\
+func collectEmbeddedMethods(mset methodSet, info *typeInfo, recvTypeName string, embeddedIsPtr bool) {\
 \tfor _, e := range info.embedded {\
 \t\tif e.typ.forward != nil { // == e was processed\
+\t\t\t// Once an embedded type was embedded as a pointer type\n+\t\t\t// all embedded types in those types are treated like\n+\t\t\t// pointer types for the purpose of the receiver type\n+\t\t\t// computation; i.e., embeddedIsPtr is sticky for this\n+\t\t\t// embedding hierarchy.\n+\t\t\tthisEmbeddedIsPtr := embeddedIsPtr || e.ptr\
 \t\t\tfor _, m := range e.typ.forward.methods {\
-\t\t\t\tmset.add(customizeRecv(m, e.ptr, recvTypeName))\n+\t\t\t\tmset.add(customizeRecv(m, thisEmbeddedIsPtr, recvTypeName))\
 \t\t\t}\
-\t\t\tcollectEmbeddedMethods(mset, e.typ, recvTypeName)\
+\t\t\tcollectEmbeddedMethods(mset, e.typ, recvTypeName, thisEmbeddedIsPtr)\
 \t\t}\
 \t}\
 }\
@@ -558,12 +605,10 @@ func customizeRecv(m *FuncDoc, embeddedIsPtr bool, recvTypeName string) *FuncDoc\
 \t}\
 \n \t// copy existing receiver field and set new type\n-\t// TODO(gri) is receiver type computation correct?\n-\t//           what about deeply nested embeddings?\n \tnewField := *m.Decl.Recv.List[0]\n \t_, origRecvIsPtr := newField.Type.(*ast.StarExpr)\n \tvar typ ast.Expr = ast.NewIdent(recvTypeName)\n-\tif embeddedIsPtr || origRecvIsPtr {\n+\tif !embeddedIsPtr && origRecvIsPtr {\
 \t\ttyp = &ast.StarExpr{token.NoPos, typ}\n \t}\n \tnewField.Type = typ\
diff --git a/src/pkg/testing/Makefile b/src/pkg/testing/Makefile
index 4b148d9717..a0c1232e36 100644
--- a/src/pkg/testing/Makefile
+++ b/src/pkg/testing/Makefile
@@ -9,6 +9,5 @@ GOFILES=\
 	benchmark.go\\
 	example.go\\
 	testing.go\\
-\twrapper.go\\
 \n include ../../Make.pkg
diff --git a/src/pkg/testing/testing.go b/src/pkg/testing/testing.go
index d75dac8f60..cfe212dc1d 100644
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -90,7 +90,7 @@ func Short() bool {\
 // If addFileLine is true, it also prefixes the string with the file and line of the call site.\
 func decorate(s string, addFileLine bool) string {\
 	if addFileLine {\
-\t\t_, file, line, ok := runtime.Caller(4) // decorate + log + public function.\
+\t\t_, file, line, ok := runtime.Caller(3) // decorate + log + public function.\
 \t\tif ok {\
 \t\t\t// Truncate file name at last file name separator.\
 \t\t\tif index := strings.LastIndex(file, \"/\"); index >= 0 {\
diff --git a/src/pkg/testing/wrapper.go b/src/pkg/testing/wrapper.go
deleted file mode 100644
index 2bef9df9c6..0000000000
--- a/src/pkg/testing/wrapper.go
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright 2009 The Go Authors. All rights reserved.\
-// Use of this source code is governed by a BSD-style\
-// license that can be found in the LICENSE file.\
-\
-// This file contains wrappers so t.Errorf etc. have documentation.\
-// TODO: delete when godoc shows exported methods for unexported embedded fields.\
-// TODO: need to change the argument to runtime.Caller in testing.go from 4 to 3 at that point.\
-\
-package testing\
-\
-// Fail marks the function as having failed but continues execution.\
-func (b *B) Fail() {\
-\tb.common.Fail()\
-}\
-\
-// Failed returns whether the function has failed.\
-func (b *B) Failed() bool {\
-\treturn b.common.Failed()\
-}\
-\
-// FailNow marks the function as having failed and stops its execution.\
-// Execution will continue at the next Test.\
-func (b *B) FailNow() {\
-\tb.common.FailNow()\
-}\
-\
-// Log formats its arguments using default formatting, analogous to Println(),\
-// and records the text in the error log.\
-func (b *B) Log(args ...interface{}) {\
-\tb.common.Log(args...)\
-}\
-\
-// Logf formats its arguments according to the format, analogous to Printf(),\
-// and records the text in the error log.\
-func (b *B) Logf(format string, args ...interface{}) {\
-\tb.common.Logf(format, args...)\
-}\
-\
-// Error is equivalent to Log() followed by Fail().\
-func (b *B) Error(args ...interface{}) {\
-\tb.common.Error(args...)\
-}\
-\n-// Errorf is equivalent to Logf() followed by Fail().\
-func (b *B) Errorf(format string, args ...interface{}) {\
-\tb.common.Errorf(format, args...)\
-}\
-\n-// Fatal is equivalent to Log() followed by FailNow().\
-func (b *B) Fatal(args ...interface{}) {\
-\tb.common.Fatal(args...)\
-}\
-\n-// Fatalf is equivalent to Logf() followed by FailNow().\
-func (b *B) Fatalf(format string, args ...interface{}) {\
-\tb.common.Fatalf(format, args...)\
-}\
-\n-// Fail marks the function as having failed but continues execution.\
-func (t *T) Fail() {\
-\tt.common.Fail()\
-}\
-\n-// Failed returns whether the function has failed.\
-func (t *T) Failed() bool {\
-\treturn t.common.Failed()\
-}\
-\n-// FailNow marks the function as having failed and stops its execution.\
-// Execution will continue at the next Test.\
-func (t *T) FailNow() {\
-\tt.common.FailNow()\
-}\
-\n-// Log formats its arguments using default formatting, analogous to Println(),\
-// and records the text in the error log.\
-func (t *T) Log(args ...interface{}) {\
-\tt.common.Log(args...)\
-}\
-\n-// Logf formats its arguments according to the format, analogous to Printf(),\
-// and records the text in the error log.\
-func (t *T) Logf(format string, args ...interface{}) {\
-\tt.common.Logf(format, args...)\
-}\
-\n-// Error is equivalent to Log() followed by Fail().\
-func (t *T) Error(args ...interface{}) {\
-\tt.common.Error(args...)\
-}\
-\n-// Errorf is equivalent to Logf() followed by Fail().\
-func (t *T) Errorf(format string, args ...interface{}) {\
-\tt.common.Errorf(format, args...)\
-}\
-\n-// Fatal is equivalent to Log() followed by FailNow().\
-func (t *T) Fatal(args ...interface{}) {\
-\tt.common.Fatal(args...)\
-}\
-\n-// Fatalf is equivalent to Logf() followed by FailNow().\
-func (t *T) Fatalf(format string, args ...interface{}) {\
-\tt.common.Fatalf(format, args...)\
-}\

コアとなるコードの解説

  • src/pkg/go/doc/doc.go:

    • NewPackageDoc関数は、パッケージのドキュメントを生成するエントリポイントです。この変更により、docReaderの初期化時にexportsOnlyというフラグが渡されるようになりました。このフラグは、生成されるドキュメントがエクスポートされた要素のみを含むべきか(trueの場合)、あるいは全ての要素を含むべきか(falseの場合)をdocReaderに伝えます。これにより、go/docの内部ロジックが、ドキュメント生成の対象範囲をより柔軟に制御できるようになります。
  • src/pkg/go/doc/exports.go:

    • filterFieldList関数は、構造体やインターフェースのフィールドリストをフィルタリングし、ドキュメントに含めるべきフィールドを決定します。この関数は、tinfo *typeInfoという新しい引数を受け取るようになりました。tinfoは、現在処理しているフィールドが属する型に関する情報を提供します。
    • 匿名フィールド(len(f.Names) == 0)の処理ロジックが拡張されました。以前は、匿名フィールドがエクスポートされている場合にのみkeepField = trueとしていましたが、変更後は、匿名フィールドが非エクスポートであっても、その型が持つメソッドを収集するために、tinfo.addEmbeddedType(embedded, ptr)を呼び出して、埋め込み型としてtypeInfoに登録するようになりました。これは、非エクスポートの埋め込み型が持つエクスポートされたメソッドもドキュメントに反映させるための最も重要な変更点です。
    • filterType関数のシグネチャもtinfo *typeInfo引数を含むように変更されました。これにより、型をフィルタリングする際に、その型が属するtypeInfoを利用できるようになり、特に構造体やインターフェースのフィールドを処理する際に、より正確な情報に基づいてフィルタリングが行えるようになりました。
  • src/pkg/go/doc/reader.go:

    • typeInfo構造体には、型の基本名を示すname stringと、その型が構造体であるかを示すisStruct boolが追加されました。これらは、ドキュメント生成時の内部処理で、型の識別と特性の判断に利用されます。
    • docReader構造体には、exportsOnly boolフィールドが追加され、init関数で初期化されるようになりました。これは、ドキュメント生成時にエクスポートされた要素のみを対象とするかどうかを制御します。
    • addFunc関数は、関数(特にメソッド)をdocReaderに追加する役割を担います。この関数内のレシーバ型処理が修正され、レシーバの型が非エクスポートであっても、それが埋め込み型として既にdoc.typesに認識されている場合は、そのメソッドを収集するようになりました。これにより、非エクスポートの埋め込み型に属するエクスポートされたメソッドも正しくドキュメントに反映されます。
    • makeTypeDocs関数は、typeInfoマップからTypeDocのリストを生成します。この関数では、old.exported() || !doc.exportsOnlyという条件が追加され、非エクスポートの型であっても、それが埋め込み型である場合や、exportsOnlyfalse(つまり全ての型をドキュメント化するモード)である場合には、その型をドキュメントリストに追加するようになりました。これにより、非エクスポートの埋め込み型が持つメソッドもドキュメントに含めることができるようになりました。
    • collectEmbeddedMethods関数は、埋め込み型からメソッドを収集する再帰関数です。この関数では、埋め込み型がポインタ型である場合のレシーバ型の計算ロジックが改善され、embeddedIsPtrという引数が追加されました。これにより、深くネストされた埋め込み型の場合でも、レシーba型が正しく解決されるようになりました。
  • src/pkg/testing/Makefilesrc/pkg/testing/testing.gosrc/pkg/testing/wrapper.go:

    • src/pkg/testing/wrapper.goは、go/docのバグに対する一時的な回避策として存在していたファイルであり、このコミットでgo/docのバグが修正されたため、完全に削除されました。
    • src/pkg/testing/Makefileからwrapper.goへの参照が削除されました。
    • src/pkg/testing/testing.godecorate関数では、runtime.Callerの引数が4から3に変更されました。これは、wrapper.goが削除されたことでコールスタックの深さが1つ減ったため、ログ出力時に正しいファイル名と行番号を取得するための調整です。

これらの変更は、Goのドキュメント生成の正確性を向上させ、Go言語の埋め込みのセマンティクスをより忠実に反映するようにgo/docの動作を修正するものです。

関連リンク

  • Go Issue #1000: godoc: don't ignore anonymous non-exported fields このコミットが修正したバグに関する公式のIssueトラッカーです。 https://github.com/golang/go/issues/1000

  • Go Code Review: go/doc: don't ignore anonymous non-exported fields このコミットのコードレビューページです。詳細な議論や変更の経緯を確認できます。 https://golang.org/cl/5502074

参考にした情報源リンク

  • Go言語の仕様 (The Go Programming Language Specification): Go言語の埋め込みやエクスポートルールに関する公式な定義。 https://go.dev/ref/spec

  • Go Doc (godoc): Goのドキュメント生成ツールに関する情報。 https://go.dev/blog/godoc

  • Goのソースコード (Go Source Code): go/docgo/astgo/tokentestingパッケージの実際のソースコード。 https://github.com/golang/go