diff --git a/godoc/godoc_test.go b/godoc/godoc_test.go
index 33dbe3f619..7f3470ed07 100644
--- a/godoc/godoc_test.go
+++ b/godoc/godoc_test.go
@@ -10,6 +10,8 @@ import (
"go/token"
"strings"
"testing"
+
+ "golang.org/x/tools/internal/typeparams"
)
func TestPkgLinkFunc(t *testing.T) {
@@ -368,3 +370,58 @@ func TestFilterOutBuildAnnotations(t *testing.T) {
t.Errorf("filterOutBuildAnnotations should not remove non-build tag comment")
}
}
+
+func TestLinkifyGenerics(t *testing.T) {
+ if !typeparams.Enabled {
+ t.Skip("type params are not enabled at this Go version")
+ }
+
+ got := linkifySource(t, []byte(`
+package foo
+
+type T struct {
+ field *T
+}
+
+type ParametricStruct[T any] struct {
+ field *T
+}
+
+func F1[T any](arg T) { }
+
+func F2(arg T) { }
+
+func (*ParametricStruct[T]) M(arg T) { }
+
+func (*T) M(arg T) { }
+
+type ParametricStruct2[T1, T2 any] struct {
+ a T1
+ b T2
+}
+
+func (*ParametricStruct2[T1, T2]) M(a T1, b T2) { }
+
+
+`))
+
+ want := `type T struct {
+field *T
+}
+type ParametricStruct[T any] struct {
+field *T
+}
+func F1[T any](arg T) {}
+func F2(arg T) {}
+func (*ParametricStruct[T]) M(arg T) {}
+func (*T) M(arg T) {}
+type ParametricStruct2[T1, T2 any] struct {
+a T1
+b T2
+}
+func (*ParametricStruct2[T1, T2]) M(a T1, b T2) {}`
+
+ if got != want {
+ t.Errorf("got: %s\n\nwant: %s\n", got, want)
+ }
+}
diff --git a/godoc/linkify.go b/godoc/linkify.go
index e4add22a10..4a9c506047 100644
--- a/godoc/linkify.go
+++ b/godoc/linkify.go
@@ -17,6 +17,8 @@ import (
"go/token"
"io"
"strconv"
+
+ "golang.org/x/tools/internal/typeparams"
)
// LinkifyText HTML-escapes source text and writes it to w.
@@ -89,6 +91,8 @@ func linksFor(node ast.Node) (links []link) {
// their ast.Ident nodes are visited.
linkMap := make(map[*ast.Ident]link)
+ typeParams := make(map[string]bool)
+
ast.Inspect(node, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.Field:
@@ -105,6 +109,24 @@ func linksFor(node ast.Node) (links []link) {
}
case *ast.FuncDecl:
linkMap[n.Name] = link{}
+ if n.Recv != nil {
+ recv := n.Recv.List[0].Type
+ if r, isstar := recv.(*ast.StarExpr); isstar {
+ recv = r.X
+ }
+ switch x := recv.(type) {
+ case *ast.IndexExpr:
+ if ident, _ := x.Index.(*ast.Ident); ident != nil {
+ typeParams[ident.Name] = true
+ }
+ case *typeparams.IndexListExpr:
+ for _, index := range x.Indices {
+ if ident, _ := index.(*ast.Ident); ident != nil {
+ typeParams[ident.Name] = true
+ }
+ }
+ }
+ }
case *ast.TypeSpec:
linkMap[n.Name] = link{}
case *ast.AssignStmt:
@@ -183,8 +205,26 @@ func linksFor(node ast.Node) (links []link) {
links = append(links, l)
} else {
l := link{name: n.Name}
- if n.Obj == nil && doc.IsPredeclared(n.Name) {
- l.path = builtinPkgPath
+ if n.Obj == nil {
+ if doc.IsPredeclared(n.Name) {
+ l.path = builtinPkgPath
+ } else {
+ if typeParams[n.Name] {
+ // If a type parameter was declared then do not generate a link.
+ // Doing this is necessary because type parameter identifiers do not
+ // have their Decl recorded sometimes, see
+ // https://golang.org/issue/50956.
+ l = link{}
+ }
+ }
+ } else {
+ if n.Obj.Kind == ast.Typ {
+ if _, isfield := n.Obj.Decl.(*ast.Field); isfield {
+ // If an identifier is a type declared in a field assume it is a type
+ // parameter and do not generate a link.
+ l = link{}
+ }
+ }
}
links = append(links, l)
}