diff --git a/src/go/doc/doc_test.go b/src/go/doc/doc_test.go index c8cdf9eb37f1c5..5a5fbd8bf3ce6a 100644 --- a/src/go/doc/doc_test.go +++ b/src/go/doc/doc_test.go @@ -160,3 +160,142 @@ func TestAnchorID(t *testing.T) { t.Errorf("anchorID(%q) = %q; want %q", in, got, want) } } + +func TestFuncs(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "funcs.go", strings.NewReader(funcsTestFile), parser.ParseComments) + if err != nil { + t.Fatal(err) + } + doc, err := NewFromFiles(fset, []*ast.File{file}, "importPath", Mode(0)) + if err != nil { + t.Fatal(err) + } + + for _, f := range doc.Funcs { + f.Decl = nil + } + for _, ty := range doc.Types { + for _, f := range ty.Funcs { + f.Decl = nil + } + for _, m := range ty.Methods { + m.Decl = nil + } + } + + compareFuncs := func(t *testing.T, msg string, got, want *Func) { + // ignore Decl and Examples + got.Decl = nil + got.Examples = nil + if !(got.Doc == want.Doc && + got.Name == want.Name && + got.Recv == want.Recv && + got.Orig == want.Orig && + got.Level == want.Level) { + t.Errorf("%s:\ngot %+v\nwant %+v", msg, got, want) + } + } + + compareSlices(t, "Funcs", doc.Funcs, funcsPackage.Funcs, compareFuncs) + compareSlices(t, "Types", doc.Types, funcsPackage.Types, func(t *testing.T, msg string, got, want *Type) { + if got.Name != want.Name { + t.Errorf("%s.Name: got %q, want %q", msg, got.Name, want.Name) + } else { + compareSlices(t, got.Name+".Funcs", got.Funcs, want.Funcs, compareFuncs) + compareSlices(t, got.Name+".Methods", got.Methods, want.Methods, compareFuncs) + } + }) +} + +func compareSlices[E any](t *testing.T, name string, got, want []E, compareElem func(*testing.T, string, E, E)) { + if len(got) != len(want) { + t.Errorf("%s: got %d, want %d", name, len(got), len(want)) + } + for i := 0; i < len(got) && i < len(want); i++ { + compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i]) + } +} + +const funcsTestFile = ` +package funcs + +func F() {} + +type S1 struct { + S2 // embedded, exported + s3 // embedded, unexported +} + +func NewS1() S1 {return S1{} } +func NewS1p() *S1 { return &S1{} } + +func (S1) M1() {} +func (r S1) M2() {} +func(S1) m3() {} // unexported not shown +func (*S1) P1() {} // pointer receiver + +type S2 int +func (S2) M3() {} // shown on S2 + +type s3 int +func (s3) M4() {} // shown on S1 + +type G1[T any] struct { + *s3 +} + +func NewG1[T any]() G1[T] { return G1[T]{} } + +func (G1[T]) MG1() {} +func (*G1[U]) MG2() {} + +type G2[T, U any] struct {} + +func NewG2[T, U any]() G2[T, U] { return G2[T, U]{} } + +func (G2[T, U]) MG3() {} +func (*G2[A, B]) MG4() {} + + +` + +var funcsPackage = &Package{ + Funcs: []*Func{{Name: "F"}}, + Types: []*Type{ + { + Name: "G1", + Funcs: []*Func{{Name: "NewG1"}}, + Methods: []*Func{ + {Name: "M4", Recv: "G1", // TODO: synthesize a param for G1? + Orig: "s3", Level: 1}, + {Name: "MG1", Recv: "G1[T]", Orig: "G1[T]", Level: 0}, + {Name: "MG2", Recv: "*G1[U]", Orig: "*G1[U]", Level: 0}, + }, + }, + { + Name: "G2", + Funcs: []*Func{{Name: "NewG2"}}, + Methods: []*Func{ + {Name: "MG3", Recv: "G2[T, U]", Orig: "G2[T, U]", Level: 0}, + {Name: "MG4", Recv: "*G2[A, B]", Orig: "*G2[A, B]", Level: 0}, + }, + }, + { + Name: "S1", + Funcs: []*Func{{Name: "NewS1"}, {Name: "NewS1p"}}, + Methods: []*Func{ + {Name: "M1", Recv: "S1", Orig: "S1", Level: 0}, + {Name: "M2", Recv: "S1", Orig: "S1", Level: 0}, + {Name: "M4", Recv: "S1", Orig: "s3", Level: 1}, + {Name: "P1", Recv: "*S1", Orig: "*S1", Level: 0}, + }, + }, + { + Name: "S2", + Methods: []*Func{ + {Name: "M3", Recv: "S2", Orig: "S2", Level: 0}, + }, + }, + }, +} diff --git a/src/go/doc/reader.go b/src/go/doc/reader.go index 7ff868f0627cc8..de1d42210640fb 100644 --- a/src/go/doc/reader.go +++ b/src/go/doc/reader.go @@ -5,11 +5,13 @@ package doc import ( + "fmt" "go/ast" "go/token" "internal/lazyregexp" "sort" "strconv" + "strings" ) // ---------------------------------------------------------------------------- @@ -22,8 +24,8 @@ import ( // type methodSet map[string]*Func -// recvString returns a string representation of recv of the -// form "T", "*T", or "BADRECV" (if not a proper receiver type). +// recvString returns a string representation of recv of the form "T", "*T", +// "T[A, ...]", "*T[A, ...]" or "BADRECV" (if not a proper receiver type). // func recvString(recv ast.Expr) string { switch t := recv.(type) { @@ -31,10 +33,34 @@ func recvString(recv ast.Expr) string { return t.Name case *ast.StarExpr: return "*" + recvString(t.X) + case *ast.IndexExpr: + // Generic type with one parameter. + return fmt.Sprintf("%s[%s]", recvString(t.X), recvParam(t.Index)) + case *ast.IndexListExpr: + // Generic type with multiple parameters. + if len(t.Indices) > 0 { + var b strings.Builder + b.WriteString(recvString(t.X)) + b.WriteByte('[') + b.WriteString(recvParam(t.Indices[0])) + for _, e := range t.Indices[1:] { + b.WriteString(", ") + b.WriteString(recvParam(e)) + } + b.WriteByte(']') + return b.String() + } } return "BADRECV" } +func recvParam(p ast.Expr) string { + if id, ok := p.(*ast.Ident); ok { + return id.Name + } + return "BADPARAM" +} + // set creates the corresponding Func for f and adds it to mset. // If there are multiple f's with the same name, set keeps the first // one with documentation; conflicts are ignored. The boolean