internal/lsp: honor GOPRIVATE in documentLinks and go.mod hovers
Several fixes related to GOPRIVATE handling and links:
+ In Go source, fix links matching GOPRIVATE for external modules.
Previously, in these cases we'd try to match <mod>@v1.2.3/<suffix>,
which wasn't the correct input into the GOPRIVATE matching algorithm.
+ Similarly check GOPRIVATE for go.mod require statement hovers.
+ Likewise, for documentLink requests (both mod and source).
+ Move the existing hover regtest to link_test.go, and expand to cover
all these cases.
Along the way, I encountered a couple apparent bugs, which I fixed:
+ Correctly handle the case where there is only one require in a go.mod
file. This was exercised by the regtest, so took some debugging.
+ Only format links [like](this) if the requested format is actually
markdown.
Fixes golang/go#36998
Change-Id: I92011821f646f2a7449dcca619483f83bdeb54b0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/238029
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index 9ac6e97..d8d618f 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -200,7 +200,7 @@
return err
}
whyList := strings.Split(stdout.String(), "\n\n")
- if len(whyList) <= 1 || len(whyList) != len(data.parsed.Require) {
+ if len(whyList) != len(data.parsed.Require) {
return nil
}
data.why = make(map[string]string, len(data.parsed.Require))
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index 5060131..96a94cc 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -760,3 +760,12 @@
}
return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil
}
+
+func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) {
+ if e.Server == nil {
+ return nil, nil
+ }
+ params := &protocol.DocumentLinkParams{}
+ params.TextDocument.URI = e.sandbox.Workdir.URI(path)
+ return e.Server.DocumentLink(ctx, params)
+}
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index ea09d76..98dc3c5 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -56,6 +56,10 @@
}
var links []protocol.DocumentLink
for _, req := range file.Require {
+ // See golang/go#36998: don't link to modules matching GOPRIVATE.
+ if snapshot.View().IsGoPrivatePath(req.Mod.Path) {
+ continue
+ }
dep := []byte(req.Mod.Path)
s, e := req.Syntax.Start.Byte, req.Syntax.End.Byte
i := bytes.Index(m.Content[s:e], dep)
@@ -132,6 +136,10 @@
if err != nil {
continue
}
+ // See golang/go#36998: don't link to modules matching GOPRIVATE.
+ if view.IsGoPrivatePath(target) {
+ continue
+ }
if mod, version, ok := moduleAtVersion(ctx, target, ph); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" {
target = strings.Replace(target, mod, mod+"@"+version, 1)
}
diff --git a/internal/lsp/mod/hover.go b/internal/lsp/mod/hover.go
index 642d1bb..2775dea 100644
--- a/internal/lsp/mod/hover.go
+++ b/internal/lsp/mod/hover.go
@@ -26,20 +26,20 @@
mh, err := snapshot.ModHandle(ctx, fh)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("getting modfile handle: %w", err)
}
file, m, why, err := mh.Why(ctx)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("running go mod why: %w", err)
}
// Get the position of the cursor.
spn, err := m.PointSpan(position)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("computing cursor position: %w", err)
}
hoverRng, err := spn.Range(m.Converter)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("computing hover range: %w", err)
}
var req *modfile.Require
@@ -85,7 +85,8 @@
return nil, err
}
options := snapshot.View().Options()
- explanation = formatExplanation(explanation, req, options)
+ isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path)
+ explanation = formatExplanation(explanation, req, options, isPrivate)
return &protocol.Hover{
Contents: protocol.MarkupContent{
Kind: options.PreferredContentFormat,
@@ -95,7 +96,7 @@
}, nil
}
-func formatExplanation(text string, req *modfile.Require, options source.Options) string {
+func formatExplanation(text string, req *modfile.Require, options source.Options, isPrivate bool) string {
text = strings.TrimSuffix(text, "\n")
splt := strings.Split(text, "\n")
length := len(splt)
@@ -117,16 +118,17 @@
return b.String()
}
- imp := splt[length-1]
- target := imp
- if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
- target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
+ imp := splt[length-1] // import path
+ reference := imp
+ // See golang/go#36998: don't link to modules matching GOPRIVATE.
+ if !isPrivate && options.PreferredContentFormat == protocol.Markdown {
+ target := imp
+ if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
+ target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
+ }
+ reference = fmt.Sprintf("[%s](https://%s/%s)", imp, options.LinkTarget, target)
}
- target = fmt.Sprintf("https://%s/%s", options.LinkTarget, target)
-
- b.WriteString("This module is necessary because ")
- msg := fmt.Sprintf("[%s](%s) is imported in", imp, target)
- b.WriteString(msg)
+ b.WriteString("This module is necessary because " + reference + " is imported in")
// If the explanation is 3 lines, then it is of the form:
// # golang.org/x/tools
diff --git a/internal/lsp/regtest/hover_test.go b/internal/lsp/regtest/hover_test.go
deleted file mode 100644
index 1281f4d..0000000
--- a/internal/lsp/regtest/hover_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright 2020 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 regtest
-
-import (
- "strings"
- "testing"
-
- "golang.org/x/tools/internal/lsp/fake"
-)
-
-const simpleProgram = `
--- go.mod --
-module gopls.test
-
-go 1.12
--- lib/lib.go --
-package lib
-
-const Hello = "Hello"
--- main.go --
-package main
-
-import (
- "fmt"
- "gopls.test/lib"
-)
-
-func main() {
- fmt.Println(lib.Hello)
-}`
-
-func TestHover(t *testing.T) {
- runner.Run(t, simpleProgram, func(t *testing.T, env *Env) {
- // Hover on an empty line.
- env.OpenFile("main.go")
- content, pos := env.Hover("main.go", fake.Pos{Line: 3, Column: 0})
- if content != nil {
- t.Errorf("got non-empty response for empty hover: %v: %v", pos, *content)
- }
- content, pos = env.Hover("main.go", env.RegexpSearch("main.go", "lib.Hello"))
- link := "pkg.go.dev/gopls.test/lib"
- if content == nil || !strings.Contains(content.Value, link) {
- t.Errorf("got hover: %v, want contains %q", content, link)
- }
- env.ChangeEnv("GOPRIVATE=gopls.test")
- content, pos = env.Hover("main.go", env.RegexpSearch("main.go", "lib.Hello"))
- if content == nil || strings.Contains(content.Value, link) {
- t.Errorf("got hover: %v, want non-empty hover without %q", content, link)
- }
- })
-}
diff --git a/internal/lsp/regtest/link_test.go b/internal/lsp/regtest/link_test.go
new file mode 100644
index 0000000..913abf4
--- /dev/null
+++ b/internal/lsp/regtest/link_test.go
@@ -0,0 +1,85 @@
+// Copyright 2020 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 regtest
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestHoverAndDocumentLink(t *testing.T) {
+ const program = `
+-- go.mod --
+module mod.test
+
+go 1.12
+
+require import.test v1.2.3
+-- main.go --
+package main
+
+import "import.test/pkg"
+
+func main() {
+ println(pkg.Hello)
+}`
+
+ const proxy = `
+-- import.test@v1.2.3/go.mod --
+module import.test
+
+go 1.12
+-- import.test@v1.2.3/pkg/const.go --
+package pkg
+
+const Hello = "Hello"
+`
+ runner.Run(t, program, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ env.OpenFile("go.mod")
+
+ modLink := "https://pkg.go.dev/mod/import.test@v1.2.3"
+ pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg"
+
+ // First, check that we get the expected links via hover and documentLink.
+ content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello"))
+ if content == nil || !strings.Contains(content.Value, pkgLink) {
+ t.Errorf("hover: got %v in main.go, want contains %q", content, pkgLink)
+ }
+ content, _ = env.Hover("go.mod", env.RegexpSearch("go.mod", "import.test"))
+ if content == nil || !strings.Contains(content.Value, pkgLink) {
+ t.Errorf("hover: got %v in go.mod, want contains %q", content, pkgLink)
+ }
+ links := env.DocumentLink("main.go")
+ if len(links) != 1 || links[0].Target != pkgLink {
+ t.Errorf("documentLink: got %v for main.go, want link to %q", links, pkgLink)
+ }
+ links = env.DocumentLink("go.mod")
+ if len(links) != 1 || links[0].Target != modLink {
+ t.Errorf("documentLink: got %v for go.mod, want link to %q", links, modLink)
+ }
+
+ // Then change the environment to make these links private.
+ env.ChangeEnv("GOPRIVATE=import.test")
+
+ // Finally, verify that the links are gone.
+ content, _ = env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello"))
+ if content == nil || strings.Contains(content.Value, pkgLink) {
+ t.Errorf("hover: got %v in main.go, want non-empty hover without %q", content, pkgLink)
+ }
+ content, _ = env.Hover("go.mod", env.RegexpSearch("go.mod", "import.test"))
+ if content == nil || strings.Contains(content.Value, modLink) {
+ t.Errorf("hover: got %v in go.mod, want contains %q", content, modLink)
+ }
+ links = env.DocumentLink("main.go")
+ if len(links) != 0 {
+ t.Errorf("documentLink: got %d document links for main.go, want 0\nlinks: %v", len(links), links)
+ }
+ links = env.DocumentLink("go.mod")
+ if len(links) != 0 {
+ t.Errorf("documentLink: got %d document links for go.mod, want 0\nlinks: %v", len(links), links)
+ }
+ }, WithProxy(proxy))
+}
diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go
index e69e7ca..f1c7502 100644
--- a/internal/lsp/regtest/wrappers.go
+++ b/internal/lsp/regtest/wrappers.go
@@ -164,6 +164,15 @@
return c, p
}
+func (e *Env) DocumentLink(name string) []protocol.DocumentLink {
+ e.T.Helper()
+ links, err := e.Editor.DocumentLink(e.Ctx, name)
+ if err != nil {
+ e.T.Fatal(err)
+ }
+ return links
+}
+
func checkIsFatal(t *testing.T, err error) {
t.Helper()
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index 95cd014..ffa85be 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -195,7 +195,7 @@
if err != nil {
return item, nil
}
- hover, err := ident.Hover(ctx)
+ hover, err := HoverIdentifier(ctx, ident)
if err != nil {
event.Error(ctx, "failed to find Hover", err, tag.URI.Of(uri))
return item, nil
diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go
index d3d26c7..8fc6974 100644
--- a/internal/lsp/source/hover.go
+++ b/internal/lsp/source/hover.go
@@ -34,6 +34,9 @@
// FullDocumentation is the symbol's full documentation.
FullDocumentation string `json:"fullDocumentation"`
+ // ImportPath is the import path for the package containing the given symbol.
+ ImportPath string
+
// Link is the pkg.go.dev anchor for the given symbol.
// For example, "go/ast#Node".
Link string `json:"link"`
@@ -50,7 +53,7 @@
if err != nil {
return nil, nil
}
- h, err := ident.Hover(ctx)
+ h, err := HoverIdentifier(ctx, ident)
if err != nil {
return nil, err
}
@@ -58,8 +61,11 @@
if err != nil {
return nil, err
}
- isPrivate := h.Link != "" && snapshot.View().IsGoPrivatePath(h.Link)
- hover, err := FormatHover(h, snapshot.View().Options(), isPrivate)
+ // See golang/go#36998: don't link to modules matching GOPRIVATE.
+ if snapshot.View().IsGoPrivatePath(h.ImportPath) {
+ h.Link = ""
+ }
+ hover, err := FormatHover(h, snapshot.View().Options())
if err != nil {
return nil, err
}
@@ -72,7 +78,7 @@
}, nil
}
-func (i *IdentifierInfo) Hover(ctx context.Context) (*HoverInformation, error) {
+func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverInformation, error) {
ctx, done := event.Start(ctx, "source.Hover")
defer done()
@@ -95,7 +101,7 @@
if obj := i.Declaration.obj; obj != nil {
h.SingleLine = objectString(obj, i.qf)
}
- h.Link, h.SymbolName = i.linkAndSymbolName()
+ h.ImportPath, h.Link, h.SymbolName = pathLinkAndSymbolName(i)
if h.comment != nil {
h.FullDocumentation = h.comment.Text()
h.Synopsis = doc.Synopsis(h.FullDocumentation)
@@ -103,32 +109,33 @@
return h, nil
}
-func (i *IdentifierInfo) linkAndSymbolName() (string, string) {
+func pathLinkAndSymbolName(i *IdentifierInfo) (string, string, string) {
obj := i.Declaration.obj
if obj == nil {
- return "", ""
+ return "", "", ""
}
switch obj := obj.(type) {
case *types.PkgName:
path := obj.Imported().Path()
+ link := path
if mod, version, ok := moduleAtVersion(path, i); ok {
- path = strings.Replace(path, mod, mod+"@"+version, 1)
+ link = strings.Replace(path, mod, mod+"@"+version, 1)
}
- return path, obj.Name()
+ return path, link, obj.Name()
case *types.Builtin:
- return fmt.Sprintf("builtin#%s", obj.Name()), obj.Name()
+ return "builtin", fmt.Sprintf("builtin#%s", obj.Name()), obj.Name()
}
// Check if the identifier is test-only (and is therefore not part of a
// package's API). This is true if the request originated in a test package,
// and if the declaration is also found in the same test package.
if i.pkg != nil && obj.Pkg() != nil && i.pkg.ForTest() != "" {
if _, pkg, _ := FindFileInPackage(i.pkg, i.Declaration.MappedRange[0].URI()); i.pkg == pkg {
- return "", ""
+ return "", "", ""
}
}
// Don't return links for other unexported types.
if !obj.Exported() {
- return "", ""
+ return "", "", ""
}
var rTypeName string
switch obj := obj.(type) {
@@ -144,7 +151,7 @@
case *types.Func:
typ, ok := obj.Type().(*types.Signature)
if !ok {
- return "", ""
+ return "", "", ""
}
if r := typ.Recv(); r != nil {
switch rtyp := deref(r.Type()).(type) {
@@ -154,7 +161,7 @@
if named, ok := i.enclosing.(*types.Named); ok {
rTypeName = named.Obj().Name()
} else if !rtyp.Obj().Exported() {
- return "", ""
+ return "", "", ""
} else {
rTypeName = rtyp.Obj().Name()
}
@@ -162,16 +169,19 @@
}
}
path := obj.Pkg().Path()
+ link := path
if mod, version, ok := moduleAtVersion(path, i); ok {
- path = strings.Replace(path, mod, mod+"@"+version, 1)
+ link = strings.Replace(path, mod, mod+"@"+version, 1)
}
if rTypeName != "" {
- link := fmt.Sprintf("%s#%s.%s", path, rTypeName, obj.Name())
+ link = fmt.Sprintf("%s#%s.%s", link, rTypeName, obj.Name())
symbol := fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), rTypeName, obj.Name())
- return link, symbol
+ return path, link, symbol
}
// For most cases, the link is "package/path#symbol".
- return fmt.Sprintf("%s#%s", path, obj.Name()), fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
+ link = fmt.Sprintf("%s#%s", link, obj.Name())
+ symbolName := fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
+ return path, link, symbolName
}
func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) {
@@ -331,7 +341,7 @@
return &HoverInformation{source: obj, comment: decl.Doc}
}
-func FormatHover(h *HoverInformation, options Options, isPrivate bool) (string, error) {
+func FormatHover(h *HoverInformation, options Options) (string, error) {
signature := h.Signature
if signature != "" && options.PreferredContentFormat == protocol.Markdown {
signature = fmt.Sprintf("```go\n%s\n```", signature)
@@ -349,10 +359,7 @@
}
return string(b), nil
}
- var link string
- if !isPrivate {
- link = formatLink(h, options)
- }
+ link := formatLink(h, options)
switch options.HoverKind {
case SynopsisDocumentation:
doc := formatDoc(h.Synopsis, options)
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 84c6ab0..e08260e 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -489,11 +489,11 @@
if err != nil {
t.Fatalf("failed for %v: %v", d.Src, err)
}
- h, err := ident.Hover(r.ctx)
+ h, err := source.HoverIdentifier(r.ctx, ident)
if err != nil {
t.Fatalf("failed for %v: %v", d.Src, err)
}
- hover, err := source.FormatHover(h, r.view.Options(), false)
+ hover, err := source.FormatHover(h, r.view.Options())
if err != nil {
t.Fatal(err)
}