Skip to content

Commit

Permalink
go/doc: handle generic receiver strings
Browse files Browse the repository at this point in the history
A receiver expression for a type with parameters may be an IndexExpr
or IndexListExpr in addition to an Ident or StarExpr. Add cases to
recvString to account for the new types.

Add tests that compare the fields of Func, and the fields of Type that
hold Funcs. These fields weren't previously tested.

Change-Id: Ia2cef51c85113422e0c5745c77dddcd53507ee51
Reviewed-on: https://go-review.googlesource.com/c/go/+/375095
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
  • Loading branch information
jba authored and jproberts committed Jun 21, 2022
1 parent 89f750c commit db46b2c
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 2 deletions.
139 changes: 139 additions & 0 deletions src/go/doc/doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
},
},
}
30 changes: 28 additions & 2 deletions src/go/doc/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
package doc

import (
"fmt"
"go/ast"
"go/token"
"internal/lazyregexp"
"sort"
"strconv"
"strings"
)

// ----------------------------------------------------------------------------
Expand All @@ -22,19 +24,43 @@ 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) {
case *ast.Ident:
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
Expand Down

0 comments on commit db46b2c

Please sign in to comment.