internal/godoc: make work with generics

We need to register a new AST node to support
encoding.

Also add a test.

For golang/go#48264

Change-Id: Ib9a0205ff4daeb55ee3447945fca513d2e4cb9c8
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/382977
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/internal/godoc/encode_ast.gen.go b/internal/godoc/encode_ast.gen.go
index a236aa3..27d3906 100644
--- a/internal/godoc/encode_ast.gen.go
+++ b/internal/godoc/encode_ast.gen.go
@@ -1604,7 +1604,7 @@
 		})
 }
 
-// Fields of ast_FuncType: Func Params Results
+// Fields of ast_FuncType: Func Params Results TypeParams
 
 func encode_ast_FuncType(e *codec.Encoder, x *ast.FuncType) {
 	if !e.StartStruct(x == nil, x) {
@@ -1622,6 +1622,10 @@
 		e.EncodeUint(2)
 		encode_ast_FieldList(e, x.Results)
 	}
+	if x.TypeParams != nil {
+		e.EncodeUint(3)
+		encode_ast_FieldList(e, x.TypeParams)
+	}
 	e.EndStruct()
 }
 
@@ -1648,6 +1652,8 @@
 			decode_ast_FieldList(d, &x.Params)
 		case 2:
 			decode_ast_FieldList(d, &x.Results)
+		case 3:
+			decode_ast_FieldList(d, &x.TypeParams)
 		default:
 			d.UnknownField("ast.FuncType", n)
 		}
@@ -2134,6 +2140,73 @@
 		})
 }
 
+// Fields of ast_IndexListExpr: X Lbrack Indices Rbrack
+
+func encode_ast_IndexListExpr(e *codec.Encoder, x *ast.IndexListExpr) {
+	if !e.StartStruct(x == nil, x) {
+		return
+	}
+	if x.X != nil {
+		e.EncodeUint(0)
+		e.EncodeAny(x.X)
+	}
+	if x.Lbrack != 0 {
+		e.EncodeUint(1)
+		e.EncodeInt(int64(x.Lbrack))
+	}
+	if x.Indices != nil {
+		e.EncodeUint(2)
+		encode_slice_ast_Expr(e, x.Indices)
+	}
+	if x.Rbrack != 0 {
+		e.EncodeUint(3)
+		e.EncodeInt(int64(x.Rbrack))
+	}
+	e.EndStruct()
+}
+
+func decode_ast_IndexListExpr(d *codec.Decoder, p **ast.IndexListExpr) {
+	proceed, ref := d.StartStruct()
+	if !proceed {
+		return
+	}
+	if ref != nil {
+		*p = ref.(*ast.IndexListExpr)
+		return
+	}
+	var x ast.IndexListExpr
+	d.StoreRef(&x)
+	for {
+		n := d.NextStructField()
+		if n < 0 {
+			break
+		}
+		switch n {
+		case 0:
+			x.X = d.DecodeAny().(ast.Expr)
+		case 1:
+			x.Lbrack = token.Pos(d.DecodeInt())
+		case 2:
+			decode_slice_ast_Expr(d, &x.Indices)
+		case 3:
+			x.Rbrack = token.Pos(d.DecodeInt())
+		default:
+			d.UnknownField("ast.IndexListExpr", n)
+		}
+		*p = &x
+	}
+}
+
+func init() {
+	codec.Register(&ast.IndexListExpr{},
+		func(e *codec.Encoder, x interface{}) { encode_ast_IndexListExpr(e, x.(*ast.IndexListExpr)) },
+		func(d *codec.Decoder) interface{} {
+			var x *ast.IndexListExpr
+			decode_ast_IndexListExpr(d, &x)
+			return x
+		})
+}
+
 // Fields of ast_InterfaceType: Interface Methods Incomplete
 
 func encode_ast_InterfaceType(e *codec.Encoder, x *ast.InterfaceType) {
@@ -3140,7 +3213,7 @@
 		})
 }
 
-// Fields of ast_TypeSpec: Doc Name Assign Type Comment
+// Fields of ast_TypeSpec: Doc Name Assign Type Comment TypeParams
 
 func encode_ast_TypeSpec(e *codec.Encoder, x *ast.TypeSpec) {
 	if !e.StartStruct(x == nil, x) {
@@ -3166,6 +3239,10 @@
 		e.EncodeUint(4)
 		encode_ast_CommentGroup(e, x.Comment)
 	}
+	if x.TypeParams != nil {
+		e.EncodeUint(5)
+		encode_ast_FieldList(e, x.TypeParams)
+	}
 	e.EndStruct()
 }
 
@@ -3196,6 +3273,8 @@
 			x.Type = d.DecodeAny().(ast.Expr)
 		case 4:
 			decode_ast_CommentGroup(d, &x.Comment)
+		case 5:
+			decode_ast_FieldList(d, &x.TypeParams)
 		default:
 			d.UnknownField("ast.TypeSpec", n)
 		}
diff --git a/internal/godoc/gen_ast.go b/internal/godoc/gen_ast.go
index dad03b5..b01d049 100644
--- a/internal/godoc/gen_ast.go
+++ b/internal/godoc/gen_ast.go
@@ -52,6 +52,7 @@
 		ast.ImportSpec{},
 		ast.IncDecStmt{},
 		ast.IndexExpr{},
+		ast.IndexListExpr{},
 		ast.InterfaceType{},
 		ast.KeyValueExpr{},
 		ast.LabeledStmt{},
diff --git a/internal/godoc/internal/doc/reader.go b/internal/godoc/internal/doc/reader.go
index 20b1e3c..a1f9141 100644
--- a/internal/godoc/internal/doc/reader.go
+++ b/internal/godoc/internal/doc/reader.go
@@ -5,10 +5,12 @@
 package doc
 
 import (
+	"fmt"
 	"go/ast"
 	"go/token"
 	"sort"
 	"strconv"
+	"strings"
 
 	"golang.org/x/pkgsite/internal/godoc/internal/lazyregexp"
 )
@@ -23,8 +25,8 @@
 //
 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) {
@@ -32,10 +34,34 @@
 		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
diff --git a/internal/godoc/render_test.go b/internal/godoc/render_test.go
index c8ea7c9..75deb33 100644
--- a/internal/godoc/render_test.go
+++ b/internal/godoc/render_test.go
@@ -36,53 +36,58 @@
 		ModulePackages:  nil,
 	}
 
-	// Use a Package created locally and without nodes removed as the desired doc.
-	p, err := packageForDir(filepath.Join("testdata", "p"), false)
-	if err != nil {
-		t.Fatal(err)
+	for _, name := range []string{"p", "generics"} {
+		t.Run(name, func(t *testing.T) {
+			// Use a Package created locally and without nodes removed as the desired doc.
+			p, err := packageForDir(filepath.Join("testdata", name), false)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			wantSyn, wantImports, _, err := p.DocInfo(ctx, name, si, mi)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			check := func(p *Package) {
+				t.Helper()
+				gotSyn, gotImports, _, err := p.DocInfo(ctx, name, si, mi)
+				if err != nil {
+					t.Fatal(err)
+				}
+				if gotSyn != wantSyn {
+					t.Errorf("synopsis: got %q, want %q", gotSyn, wantSyn)
+				}
+				if !cmp.Equal(gotImports, wantImports) {
+					t.Errorf("imports: got %v, want %v", gotImports, wantImports)
+				}
+			}
+
+			// Verify that removing AST nodes doesn't change the doc.
+			p, err = packageForDir(filepath.Join("testdata", name), true)
+			if err != nil {
+				t.Fatal(err)
+			}
+			check(p)
+
+			// Verify that encoding then decoding generates the same doc.
+			// We can't re-use p to encode because it's been rendered.
+			p, err = packageForDir(filepath.Join("testdata", name), true)
+			if err != nil {
+				t.Fatal(err)
+			}
+			bytes, err := p.Encode(ctx)
+			if err != nil {
+				t.Fatal(err)
+			}
+			p2, err := DecodePackage(bytes)
+			if err != nil {
+				t.Fatal(err)
+			}
+			check(p2)
+		})
 	}
 
-	wantSyn, wantImports, _, err := p.DocInfo(ctx, "p", si, mi)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	check := func(p *Package) {
-		t.Helper()
-		gotSyn, gotImports, _, err := p.DocInfo(ctx, "p", si, mi)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if gotSyn != wantSyn {
-			t.Errorf("synopsis: got %q, want %q", gotSyn, wantSyn)
-		}
-		if !cmp.Equal(gotImports, wantImports) {
-			t.Errorf("imports: got %v, want %v", gotImports, wantImports)
-		}
-	}
-
-	// Verify that removing AST nodes doesn't change the doc.
-	p, err = packageForDir(filepath.Join("testdata", "p"), true)
-	if err != nil {
-		t.Fatal(err)
-	}
-	check(p)
-
-	// Verify that encoding then decoding generates the same doc.
-	// We can't re-use p to encode because it's been rendered.
-	p, err = packageForDir(filepath.Join("testdata", "p"), true)
-	if err != nil {
-		t.Fatal(err)
-	}
-	bytes, err := p.Encode(ctx)
-	if err != nil {
-		t.Fatal(err)
-	}
-	p2, err := DecodePackage(bytes)
-	if err != nil {
-		t.Fatal(err)
-	}
-	check(p2)
 }
 
 func TestRenderParts_SinceVersion(t *testing.T) {
diff --git a/internal/godoc/testdata/generics/g.go b/internal/godoc/testdata/generics/g.go
new file mode 100644
index 0000000..fa7cc53
--- /dev/null
+++ b/internal/godoc/testdata/generics/g.go
@@ -0,0 +1,16 @@
+// Copyright 2021 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.
+
+// Package generics uses generics.
+package generics
+
+type Pair[A, B any] struct {
+	V0 A
+	V1 B
+}
+
+// NewPair returns a new Pair.
+func NewPair[A, B any](a A, b B) Pair[A, B] {
+	return Pair[A, B]{a, b}
+}