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) }