Skip to content

Commit f4878ba

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/golang: use correct imports in HTML pkg doc links
This CL fixes two bugs in the rendering of Doc Links in doc comments in the web-based "Browse package documentation" feature: 1. Doc links were resolved based on an arbitrary file in the package, even though different files have different import mappings. Now, the correct file's mapping is used 2. In the presence of a renaming import, doc links often use the original package name, not the local package name. Now, the local name is preferred, but if none is found the original name is used. + tests of both fixes. (This CL was an attempt to get some value out of a fruitless attempt to fix golang/go#61677 that turned out to be more complex than expected. The not-yet-common logic has moved out of PackageDocHTML into comments.go.) Updates golang/go#61677 Change-Id: Iebd1b11786759fbfad40c8c3b0107c80fb90ebf7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/623637 LUCI-TryBot-Result: Go LUCI <[email protected]> Commit-Queue: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]>
1 parent 109c5fc commit f4878ba

File tree

6 files changed

+188
-76
lines changed

6 files changed

+188
-76
lines changed

gopls/internal/golang/comment.go

+83-13
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,29 @@ import (
1818
"golang.org/x/tools/gopls/internal/cache/parsego"
1919
"golang.org/x/tools/gopls/internal/protocol"
2020
"golang.org/x/tools/gopls/internal/settings"
21+
"golang.org/x/tools/gopls/internal/util/astutil"
2122
"golang.org/x/tools/gopls/internal/util/safetoken"
2223
)
2324

2425
var errNoCommentReference = errors.New("no comment reference found")
2526

26-
// CommentToMarkdown converts comment text to formatted markdown.
27-
// The comment was prepared by DocReader,
28-
// so it is known not to have leading, trailing blank lines
29-
// nor to have trailing spaces at the end of lines.
30-
// The comment markers have already been removed.
31-
func CommentToMarkdown(text string, options *settings.Options) string {
32-
var p comment.Parser
33-
doc := p.Parse(text)
34-
var pr comment.Printer
27+
// DocCommentToMarkdown converts the text of a [doc comment] to Markdown.
28+
//
29+
// TODO(adonovan): provide a package (or file imports) as context for
30+
// proper rendering of doc links; see [newDocCommentParser] and golang/go#61677.
31+
//
32+
// [doc comment]: https://go.dev/doc/comment
33+
func DocCommentToMarkdown(text string, options *settings.Options) string {
34+
var parser comment.Parser
35+
doc := parser.Parse(text)
36+
37+
var printer comment.Printer
3538
// The default produces {#Hdr-...} tags for headings.
3639
// vscode displays thems, which is undesirable.
3740
// The godoc for comment.Printer says the tags
3841
// avoid a security problem.
39-
pr.HeadingID = func(*comment.Heading) string { return "" }
40-
pr.DocLinkURL = func(link *comment.DocLink) string {
42+
printer.HeadingID = func(*comment.Heading) string { return "" }
43+
printer.DocLinkURL = func(link *comment.DocLink) string {
4144
msg := fmt.Sprintf("https://%s/%s", options.LinkTarget, link.ImportPath)
4245
if link.Name != "" {
4346
msg += "#"
@@ -48,8 +51,8 @@ func CommentToMarkdown(text string, options *settings.Options) string {
4851
}
4952
return msg
5053
}
51-
easy := pr.Markdown(doc)
52-
return string(easy)
54+
55+
return string(printer.Markdown(doc))
5356
}
5457

5558
// docLinkDefinition finds the definition of the doc link in comments at pos.
@@ -199,3 +202,70 @@ func lookupDocLinkSymbol(pkg *cache.Package, pgf *parsego.File, name string) typ
199202
// package-level symbol
200203
return scope.Lookup(name)
201204
}
205+
206+
// newDocCommentParser returns a function that parses [doc comments],
207+
// with context for Doc Links supplied by the specified package.
208+
//
209+
// Imported symbols are rendered using the import mapping for the file
210+
// that encloses fileNode.
211+
//
212+
// The resulting function is not concurrency safe.
213+
//
214+
// See issue #61677 for how this might be generalized to support
215+
// correct contextual parsing of doc comments in Hover too.
216+
//
217+
// [doc comment]: https://go.dev/doc/comment
218+
func newDocCommentParser(pkg *cache.Package) func(fileNode ast.Node, text string) *comment.Doc {
219+
var currentFileNode ast.Node // node whose enclosing file's import mapping should be used
220+
parser := &comment.Parser{
221+
LookupPackage: func(name string) (importPath string, ok bool) {
222+
for _, f := range pkg.Syntax() {
223+
// Different files in the same package have
224+
// different import mappings. Use the provided
225+
// syntax node to find the correct file.
226+
if astutil.NodeContains(f, currentFileNode.Pos()) {
227+
// First try the actual imported package name.
228+
for _, imp := range f.Imports {
229+
pkgName := pkg.TypesInfo().PkgNameOf(imp)
230+
if pkgName != nil && pkgName.Name() == name {
231+
return pkgName.Imported().Path(), true
232+
}
233+
}
234+
235+
// Then try the imported package name, as some
236+
// packages are typically imported under a
237+
// non-default name (e.g. pathpkg "path") but
238+
// may be referred to in doc links using their
239+
// canonical name.
240+
for _, imp := range f.Imports {
241+
pkgName := pkg.TypesInfo().PkgNameOf(imp)
242+
if pkgName != nil && pkgName.Imported().Name() == name {
243+
return pkgName.Imported().Path(), true
244+
}
245+
}
246+
247+
break
248+
}
249+
}
250+
return "", false
251+
},
252+
LookupSym: func(recv, name string) (ok bool) {
253+
// package-level decl?
254+
if recv == "" {
255+
return pkg.Types().Scope().Lookup(name) != nil
256+
}
257+
258+
// method?
259+
tname, ok := pkg.Types().Scope().Lookup(recv).(*types.TypeName)
260+
if !ok {
261+
return false
262+
}
263+
m, _, _ := types.LookupFieldOrMethod(tname.Type(), true, pkg.Types(), name)
264+
return is[*types.Func](m)
265+
},
266+
}
267+
return func(fileNode ast.Node, text string) *comment.Doc {
268+
currentFileNode = fileNode
269+
return parser.Parse(text)
270+
}
271+
}

gopls/internal/golang/hover.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ import (
5252
// TODO(adonovan): see if we can wean all clients of this interface.
5353
type hoverJSON struct {
5454
// Synopsis is a single sentence synopsis of the symbol's documentation.
55+
//
56+
// TODO(adonovan): in what syntax? It (usually) comes from doc.Synopsis,
57+
// which produces "Text" form, but it may be fed to
58+
// DocCommentToMarkdown, which expects doc comment syntax.
5559
Synopsis string `json:"synopsis"`
5660

5761
// FullDocumentation is the symbol's full documentation.
@@ -1318,7 +1322,7 @@ func formatDoc(h *hoverJSON, options *settings.Options) string {
13181322
doc = h.FullDocumentation
13191323
}
13201324
if options.PreferredContentFormat == protocol.Markdown {
1321-
return CommentToMarkdown(doc, options)
1325+
return DocCommentToMarkdown(doc, options)
13221326
}
13231327
return doc
13241328
}

gopls/internal/golang/pkgdoc.go

+31-60
Original file line numberDiff line numberDiff line change
@@ -326,68 +326,34 @@ func PackageDocHTML(viewID string, pkg *cache.Package, web Web) ([]byte, error)
326326
})
327327
}
328328

329-
var docHTML func(comment string) []byte
329+
// docHTML renders the doc comment as Markdown.
330+
// The fileNode is used to deduce the enclosing file
331+
// for the correct import mapping.
332+
//
333+
// It is not concurrency-safe.
334+
var docHTML func(fileNode ast.Node, comment string) []byte
330335
{
331336
// Adapt doc comment parser and printer
332337
// to our representation of Go packages
333338
// so that doc links (e.g. "[fmt.Println]")
334339
// become valid links.
335-
336-
printer := docpkg.Printer()
337-
printer.DocLinkURL = func(link *comment.DocLink) string {
338-
path := pkg.Metadata().PkgPath
339-
if link.ImportPath != "" {
340-
path = PackagePath(link.ImportPath)
341-
}
342-
fragment := link.Name
343-
if link.Recv != "" {
344-
fragment = link.Recv + "." + link.Name
345-
}
346-
return web.PkgURL(viewID, path, fragment)
347-
}
348-
parser := docpkg.Parser()
349-
parser.LookupPackage = func(name string) (importPath string, ok bool) {
350-
// Ambiguous: different files in the same
351-
// package may have different import mappings,
352-
// but the hook doesn't provide the file context.
353-
// TODO(adonovan): conspire with docHTML to
354-
// pass the doc comment's enclosing file through
355-
// a shared variable, so that we can compute
356-
// the correct per-file mapping.
357-
//
358-
// TODO(adonovan): check for PkgName.Name
359-
// matches, but also check for
360-
// PkgName.Imported.Namer matches, since some
361-
// packages are typically imported under a
362-
// non-default name (e.g. pathpkg "path") but
363-
// may be referred to in doc links using their
364-
// canonical name.
365-
for _, f := range pkg.Syntax() {
366-
for _, imp := range f.Imports {
367-
pkgName := pkg.TypesInfo().PkgNameOf(imp)
368-
if pkgName != nil && pkgName.Name() == name {
369-
return pkgName.Imported().Path(), true
370-
}
340+
printer := &comment.Printer{
341+
DocLinkURL: func(link *comment.DocLink) string {
342+
path := pkg.Metadata().PkgPath
343+
if link.ImportPath != "" {
344+
path = PackagePath(link.ImportPath)
371345
}
372-
}
373-
return "", false
374-
}
375-
parser.LookupSym = func(recv, name string) (ok bool) {
376-
// package-level decl?
377-
if recv == "" {
378-
return pkg.Types().Scope().Lookup(name) != nil
379-
}
380-
381-
// method?
382-
tname, ok := pkg.Types().Scope().Lookup(recv).(*types.TypeName)
383-
if !ok {
384-
return false
385-
}
386-
m, _, _ := types.LookupFieldOrMethod(tname.Type(), true, pkg.Types(), name)
387-
return is[*types.Func](m)
346+
fragment := link.Name
347+
if link.Recv != "" {
348+
fragment = link.Recv + "." + link.Name
349+
}
350+
return web.PkgURL(viewID, path, fragment)
351+
},
388352
}
389-
docHTML = func(comment string) []byte {
390-
return printer.HTML(parser.Parse(comment))
353+
parse := newDocCommentParser(pkg)
354+
docHTML = func(fileNode ast.Node, comment string) []byte {
355+
doc := parse(fileNode, comment)
356+
return printer.HTML(doc)
391357
}
392358
}
393359

@@ -715,7 +681,12 @@ window.addEventListener('load', function() {
715681
"https://pkg.go.dev/"+string(pkg.Types().Path()))
716682

717683
// package doc
718-
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(docpkg.Doc))
684+
for _, f := range pkg.Syntax() {
685+
if f.Doc != nil {
686+
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(f.Doc, docpkg.Doc))
687+
break
688+
}
689+
}
719690

720691
// symbol index
721692
fmt.Fprintf(&buf, "<h2 id='hdr-Index'>Index</h2>\n")
@@ -773,7 +744,7 @@ window.addEventListener('load', function() {
773744
fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", nodeHTML(&decl2))
774745

775746
// comment (if any)
776-
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(v.Doc))
747+
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(v.Decl, v.Doc))
777748
}
778749
}
779750
fmt.Fprintf(&buf, "<h2 id='hdr-Constants'>Constants</h2>\n")
@@ -814,7 +785,7 @@ window.addEventListener('load', function() {
814785
nodeHTML(docfn.Decl.Type))
815786

816787
// comment (if any)
817-
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(docfn.Doc))
788+
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(docfn.Decl, docfn.Doc))
818789
}
819790
}
820791
funcs(docpkg.Funcs)
@@ -835,7 +806,7 @@ window.addEventListener('load', function() {
835806
fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", nodeHTML(&decl2))
836807

837808
// comment (if any)
838-
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(doctype.Doc))
809+
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(doctype.Decl, doctype.Doc))
839810

840811
// subelements
841812
values(doctype.Consts) // constants of type T
@@ -856,7 +827,7 @@ window.addEventListener('load', function() {
856827

857828
// comment (if any)
858829
fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n",
859-
docHTML(docmethod.Doc))
830+
docHTML(docmethod.Decl, docmethod.Doc))
860831
}
861832
}
862833

gopls/internal/golang/signature_help.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func stringToSigInfoDocumentation(s string, options *settings.Options) *protocol
223223
v := s
224224
k := protocol.PlainText
225225
if options.PreferredContentFormat == protocol.Markdown {
226-
v = CommentToMarkdown(s, options)
226+
v = DocCommentToMarkdown(s, options)
227227
// whether or not content is newline terminated may not matter for LSP clients,
228228
// but our tests expect trailing newlines to be stripped.
229229
v = strings.TrimSuffix(v, "\n") // TODO(pjw): change the golden files

gopls/internal/server/completion.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func toProtocolCompletionItems(candidates []completion.CompletionItem, surroundi
143143
doc := &protocol.Or_CompletionItem_documentation{
144144
Value: protocol.MarkupContent{
145145
Kind: protocol.Markdown,
146-
Value: golang.CommentToMarkdown(candidate.Documentation, options),
146+
Value: golang.DocCommentToMarkdown(candidate.Documentation, options),
147147
},
148148
}
149149
if options.PreferredContentFormat != protocol.Markdown {

gopls/internal/test/integration/misc/webserver_test.go

+67
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"golang.org/x/tools/internal/testenv"
2222
)
2323

24+
// TODO(adonovan): define marker test verbs for checking package docs.
25+
2426
// TestWebServer exercises the web server created on demand
2527
// for code actions such as "Browse package documentation".
2628
func TestWebServer(t *testing.T) {
@@ -267,6 +269,71 @@ func (*T) M() { /*in T.M*/}
267269
})
268270
}
269271

272+
// TestPkgDocFileImports tests that the doc links are rendered
273+
// as URLs based on the correct import mapping for the file in
274+
// which they appear.
275+
func TestPkgDocFileImports(t *testing.T) {
276+
const files = `
277+
-- go.mod --
278+
module mod.com
279+
go 1.20
280+
281+
-- a/a1.go --
282+
package a
283+
284+
import "b"
285+
import alias "d"
286+
287+
// [b.T] indeed refers to b.T.
288+
//
289+
// [alias.D] refers to d.D
290+
// but [d.D] also refers to d.D.
291+
type A1 int
292+
293+
-- a/a2.go --
294+
package a
295+
296+
import b "c"
297+
298+
// [b.U] actually refers to c.U.
299+
type A2 int
300+
301+
-- b/b.go --
302+
package b
303+
304+
type T int
305+
type U int
306+
307+
-- c/c.go --
308+
package c
309+
310+
type T int
311+
type U int
312+
313+
-- d/d.go --
314+
package d
315+
316+
type D int
317+
`
318+
Run(t, files, func(t *testing.T, env *Env) {
319+
env.OpenFile("a/a1.go")
320+
uri1 := viewPkgDoc(t, env, env.Sandbox.Workdir.EntireFile("a/a1.go"))
321+
doc := get(t, uri1)
322+
323+
// Check that the doc links are resolved using the
324+
// appropriate import mapping for the file in which
325+
// they appear.
326+
checkMatch(t, true, doc, `pkg/b\?.*#T">b.T</a> indeed refers to b.T`)
327+
checkMatch(t, true, doc, `pkg/c\?.*#U">b.U</a> actually refers to c.U`)
328+
329+
// Check that doc links can be resolved using either
330+
// the original or the local name when they refer to a
331+
// renaming import. (Local names are preferred.)
332+
checkMatch(t, true, doc, `pkg/d\?.*#D">alias.D</a> refers to d.D`)
333+
checkMatch(t, true, doc, `pkg/d\?.*#D">d.D</a> also refers to d.D`)
334+
})
335+
}
336+
270337
// viewPkgDoc invokes the "Browse package documentation" code action
271338
// at the specified location. It returns the URI of the document, or
272339
// fails the test.

0 commit comments

Comments
 (0)