internal/lsp/source: don't link to packages matching GOPRIVATE in hover

Currently, our hover text by default links point to public documentation
sites (e.g. pkg.go.dev). This doesn't make sense for private repos, so
hide the hovertext link when the import path matches GOPRIVATE.

Implementing this was a little messy. To be optimal I had to thread
the value of goprivate through cache.view, and to be correct I had to
duplicate some code from cmd/go internal.

Regtest will follow after https://golang.org/cl/232983 is submitted.

Updates golang/go#36998

Change-Id: I1e556471bf919fea30132d9642426a08fdb7f434
Reviewed-on: https://go-review.googlesource.com/c/tools/+/233524
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Rob Findley 2020-05-12 21:56:33 -04:00 committed by Robert Findley
parent 7d3b6ebf13
commit 8ddc06776e
4 changed files with 74 additions and 9 deletions

View File

@ -13,6 +13,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
@ -108,7 +109,7 @@ type view struct {
goCommand bool goCommand bool
// `go env` variables that need to be tracked. // `go env` variables that need to be tracked.
gopath, gocache string gopath, gocache, goprivate string
// gocmdRunner guards go command calls from concurrency errors. // gocmdRunner guards go command calls from concurrency errors.
gocmdRunner *gocommand.Runner gocmdRunner *gocommand.Runner
@ -709,10 +710,11 @@ func isSubdirectory(root, leaf string) bool {
return err == nil && !strings.HasPrefix(rel, "..") return err == nil && !strings.HasPrefix(rel, "..")
} }
// getGoEnv sets the view's build information's GOPATH, GOCACHE, and GOPACKAGESDRIVER values. // getGoEnv sets the view's build information's GOPATH, GOCACHE, GOPRIVATE, and
// It also returns the view's GOMOD value, which need not be cached. // GOPACKAGESDRIVER values. It also returns the view's GOMOD value, which need
// not be cached.
func (v *view) getGoEnv(ctx context.Context, env []string) (string, error) { func (v *view) getGoEnv(ctx context.Context, env []string) (string, error) {
var gocache, gopath, gopackagesdriver bool var gocache, gopath, gopackagesdriver, goprivate bool
isGoCommand := func(gopackagesdriver string) bool { isGoCommand := func(gopackagesdriver string) bool {
return gopackagesdriver == "" || gopackagesdriver == "off" return gopackagesdriver == "" || gopackagesdriver == "off"
} }
@ -728,6 +730,9 @@ func (v *view) getGoEnv(ctx context.Context, env []string) (string, error) {
case "GOPATH": case "GOPATH":
v.gopath = split[1] v.gopath = split[1]
gopath = true gopath = true
case "GOPRIVATE":
v.goprivate = split[1]
goprivate = true
case "GOPACKAGESDRIVER": case "GOPACKAGESDRIVER":
v.goCommand = isGoCommand(split[1]) v.goCommand = isGoCommand(split[1])
gopackagesdriver = true gopackagesdriver = true
@ -762,6 +767,12 @@ func (v *view) getGoEnv(ctx context.Context, env []string) (string, error) {
return "", errors.New("unable to determine GOCACHE") return "", errors.New("unable to determine GOCACHE")
} }
} }
if !goprivate {
if goprivate, ok := envMap["GOPRIVATE"]; ok {
v.goprivate = goprivate
}
// No error here: GOPRIVATE is not essential.
}
// The value of GOPACKAGESDRIVER is not returned through the go command. // The value of GOPACKAGESDRIVER is not returned through the go command.
if !gopackagesdriver { if !gopackagesdriver {
v.goCommand = isGoCommand(os.Getenv("GOPACKAGESDRIVER")) v.goCommand = isGoCommand(os.Getenv("GOPACKAGESDRIVER"))
@ -770,6 +781,53 @@ func (v *view) getGoEnv(ctx context.Context, env []string) (string, error) {
return gomod, nil return gomod, nil
} }
return "", nil return "", nil
}
func (v *view) IsGoPrivatePath(target string) bool {
return globsMatchPath(v.goprivate, target)
}
// Copied from
// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/str/path.go;l=58;drc=2910c5b4a01a573ebc97744890a07c1a3122c67a
func globsMatchPath(globs, target string) bool {
for globs != "" {
// Extract next non-empty glob in comma-separated list.
var glob string
if i := strings.Index(globs, ","); i >= 0 {
glob, globs = globs[:i], globs[i+1:]
} else {
glob, globs = globs, ""
}
if glob == "" {
continue
}
// A glob with N+1 path elements (N slashes) needs to be matched
// against the first N+1 path elements of target,
// which end just before the N+1'th slash.
n := strings.Count(glob, "/")
prefix := target
// Walk target, counting slashes, truncating at the N+1'th slash.
for i := 0; i < len(target); i++ {
if target[i] == '/' {
if n == 0 {
prefix = target[:i]
break
}
n--
}
}
if n > 0 {
// Not enough prefix elements.
continue
}
matched, _ := path.Match(glob, prefix)
if matched {
return true
}
}
return false
} }
// This function will return the main go.mod file for this folder if it exists and whether the -modfile // This function will return the main go.mod file for this folder if it exists and whether the -modfile

View File

@ -58,7 +58,8 @@ func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position proto
if err != nil { if err != nil {
return nil, err return nil, err
} }
hover, err := FormatHover(h, snapshot.View().Options()) isPrivate := h.Link != "" && snapshot.View().IsGoPrivatePath(h.Link)
hover, err := FormatHover(h, snapshot.View().Options(), isPrivate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -330,7 +331,7 @@ func formatVar(node ast.Spec, obj types.Object, decl *ast.GenDecl) *HoverInforma
return &HoverInformation{source: obj, comment: decl.Doc} return &HoverInformation{source: obj, comment: decl.Doc}
} }
func FormatHover(h *HoverInformation, options Options) (string, error) { func FormatHover(h *HoverInformation, options Options, isPrivate bool) (string, error) {
signature := h.Signature signature := h.Signature
if signature != "" && options.PreferredContentFormat == protocol.Markdown { if signature != "" && options.PreferredContentFormat == protocol.Markdown {
signature = fmt.Sprintf("```go\n%s\n```", signature) signature = fmt.Sprintf("```go\n%s\n```", signature)
@ -348,7 +349,10 @@ func FormatHover(h *HoverInformation, options Options) (string, error) {
} }
return string(b), nil return string(b), nil
} }
link := formatLink(h, options) var link string
if !isPrivate {
link = formatLink(h, options)
}
switch options.HoverKind { switch options.HoverKind {
case SynopsisDocumentation: case SynopsisDocumentation:
doc := formatDoc(h.Synopsis, options) doc := formatDoc(h.Synopsis, options)
@ -374,7 +378,6 @@ func formatLink(h *HoverInformation, options Options) string {
return plainLink return plainLink
} }
} }
func formatDoc(doc string, options Options) string { func formatDoc(doc string, options Options) string {
if options.PreferredContentFormat == protocol.Markdown { if options.PreferredContentFormat == protocol.Markdown {
return CommentToMarkdown(doc) return CommentToMarkdown(doc)

View File

@ -493,7 +493,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
if err != nil { if err != nil {
t.Fatalf("failed for %v: %v", d.Src, err) t.Fatalf("failed for %v: %v", d.Src, err)
} }
hover, err := source.FormatHover(h, r.view.Options()) hover, err := source.FormatHover(h, r.view.Options(), false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -152,6 +152,10 @@ type View interface {
// user's workspace. In particular, if they are both outside of a module // user's workspace. In particular, if they are both outside of a module
// and their GOPATH. // and their GOPATH.
ValidBuildConfiguration() bool ValidBuildConfiguration() bool
// IsGoPrivatePath reports whether target is a private import path, as identified
// by the GOPRIVATE environment variable.
IsGoPrivatePath(path string) bool
} }
// Session represents a single connection from a client. // Session represents a single connection from a client.