Skip to content

Commit

Permalink
internal/lsp: add module versions from "go list" to pkg.go.dev links
Browse files Browse the repository at this point in the history
This change appends to the pkg.go.dev link the version of the module that is being used. To get this functionality, go/packages.Package now contains a module field which gets populated from the "go list" call. This module field is then used to get the version of the module that we are linking to.

Updates golang/go#36501

Change-Id: I9668a6da0fd3ec8f4cde017986419c8d28196765
Reviewed-on: https://go-review.googlesource.com/c/tools/+/219079
Run-TryBot: Rohan Challa <rohan@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
  • Loading branch information
ridersofrohan committed Feb 19, 2020
1 parent c4d4ea9 commit 7c4b627
Show file tree
Hide file tree
Showing 15 changed files with 137 additions and 28 deletions.
3 changes: 3 additions & 0 deletions go/packages/golist.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"unicode"

"golang.org/x/tools/go/internal/packagesdriver"
"golang.org/x/tools/internal/packagesinternal"
)

// debug controls verbose logging.
Expand Down Expand Up @@ -380,6 +381,7 @@ type jsonPackage struct {
Imports []string
ImportMap map[string]string
Deps []string
Module *packagesinternal.Module
TestGoFiles []string
TestImports []string
XTestGoFiles []string
Expand Down Expand Up @@ -529,6 +531,7 @@ func (state *golistState) createDriverResponse(words ...string) (*driverResponse
CompiledGoFiles: absJoin(p.Dir, p.CompiledGoFiles),
OtherFiles: absJoin(p.Dir, otherFiles(p)...),
forTest: p.ForTest,
module: p.Module,
}

// Work around https://golang.org/issue/28749:
Expand Down
6 changes: 6 additions & 0 deletions go/packages/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,18 @@ type Package struct {

// forTest is the package under test, if any.
forTest string

// module is the module information for the package if it exists.
module *packagesinternal.Module
}

func init() {
packagesinternal.GetForTest = func(p interface{}) string {
return p.(*Package).forTest
}
packagesinternal.GetModule = func(p interface{}) *packagesinternal.Module {
return p.(*Package).module
}
}

// An Error describes a problem with a package's metadata, syntax, or types.
Expand Down
4 changes: 2 additions & 2 deletions go/packages/packagestest/expect.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func (e *Exported) getNotes() error {
// matches the counts in the summary.txt.golden file for the test directory.
if gomod, found := e.written[e.primary]["go.mod"]; found {
// If we are in Modules mode, then we need to check the contents of the go.mod.temp.
if e.exporter == Modules {
if e.Exporter == Modules {
gomod += ".temp"
}
l, err := goModMarkers(e, gomod)
Expand All @@ -194,7 +194,7 @@ func goModMarkers(e *Exported, gomod string) ([]*expect.Note, error) {
if err != nil {
return nil, err
}
if e.exporter == GOPATH {
if e.Exporter == GOPATH {
return expect.Parse(e.ExpectFileSet, gomod, content)
}
gomod = strings.TrimSuffix(gomod, ".temp")
Expand Down
4 changes: 2 additions & 2 deletions go/packages/packagestest/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ type Exported struct {

ExpectFileSet *token.FileSet // The file set used when parsing expectations

exporter Exporter // the exporter used
Exporter Exporter // the exporter used
temp string // the temporary directory that was exported to
primary string // the first non GOROOT module that was exported
written map[string]map[string]string // the full set of exported files
Expand Down Expand Up @@ -201,7 +201,7 @@ func Export(t testing.TB, exporter Exporter, modules []Module) *Exported {
Mode: packages.LoadImports,
},
Modules: modules,
exporter: exporter,
Exporter: exporter,
temp: temp,
primary: modules[0].Name,
written: map[string]map[string]string{},
Expand Down
1 change: 1 addition & 0 deletions internal/lsp/cache/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ func typeCheck(ctx context.Context, fset *token.FileSet, m *metadata, mode sourc
mode: mode,
goFiles: goFiles,
compiledGoFiles: compiledGoFiles,
module: m.module,
imports: make(map[packagePath]*pkg),
typesSizes: m.typesSizes,
typesInfo: &types.Info{
Expand Down
2 changes: 2 additions & 0 deletions internal/lsp/cache/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type metadata struct {
errors []packages.Error
deps []packageID
missingDeps map[packagePath]struct{}
module *packagesinternal.Module

// config is the *packages.Config associated with the loaded package.
config *packages.Config
Expand Down Expand Up @@ -144,6 +145,7 @@ func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *pa
typesSizes: pkg.TypesSizes,
errors: pkg.Errors,
config: cfg,
module: packagesinternal.GetModule(pkg),
}

for _, filename := range pkg.CompiledGoFiles {
Expand Down
6 changes: 6 additions & 0 deletions internal/lsp/cache/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
Expand All @@ -26,6 +27,7 @@ type pkg struct {
compiledGoFiles []source.ParseGoHandle
errors []*source.Error
imports map[packagePath]*pkg
module *packagesinternal.Module
types *types.Package
typesInfo *types.Info
typesSizes types.Sizes
Expand Down Expand Up @@ -119,6 +121,10 @@ func (p *pkg) Imports() []source.Package {
return result
}

func (p *pkg) Module() *packagesinternal.Module {
return p.module
}

func (s *snapshot) FindAnalysisError(ctx context.Context, pkgID, analyzerName, msg string, rng protocol.Range) (*source.Error, error) {
analyzer, ok := s.View().Options().Analyzers[analyzerName]
if !ok {
Expand Down
37 changes: 18 additions & 19 deletions internal/lsp/cmd/test/cmdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"bytes"
"context"
"fmt"
"io/ioutil"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"

"golang.org/x/tools/go/packages/packagestest"
Expand Down Expand Up @@ -125,16 +126,18 @@ func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
t.Fatal(err)
}
oldStderr := os.Stderr
defer func() {
os.Stdout = oldStdout
os.Stderr = oldStderr
wStdout.Close()
rStdout.Close()
wStderr.Close()
rStderr.Close()
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
var wg sync.WaitGroup
wg.Add(2)
go func() {
io.Copy(stdout, rStdout)
wg.Done()
}()
os.Stdout = wStdout
os.Stderr = wStderr
go func() {
io.Copy(stderr, rStderr)
wg.Done()
}()
os.Stdout, os.Stderr = wStdout, wStderr
app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options)
remote := r.remote
err = tool.Run(tests.Context(t),
Expand All @@ -145,15 +148,11 @@ func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
}
wStdout.Close()
wStderr.Close()
stdout, err := ioutil.ReadAll(rStdout)
if err != nil {
t.Fatal(err)
}
stderr, err := ioutil.ReadAll(rStderr)
if err != nil {
t.Fatal(err)
}
return string(stdout), string(stderr)
wg.Wait()
os.Stdout, os.Stderr = oldStdout, oldStderr
rStdout.Close()
rStderr.Close()
return stdout.String(), stderr.String()
}

// NormalizeGoplsCmd runs the gopls command and normalizes its output.
Expand Down
32 changes: 31 additions & 1 deletion internal/lsp/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/url"
"regexp"
"strconv"
"strings"
"sync"

"golang.org/x/tools/internal/lsp/protocol"
Expand All @@ -27,6 +28,14 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
return nil, err
}
view := snapshot.View()
phs, err := view.Snapshot().PackageHandles(ctx, fh)
if err != nil {
return nil, err
}
ph, err := source.WidestPackageHandle(phs)
if err != nil {
return nil, err
}
file, _, m, _, err := view.Session().Cache().ParseGoHandle(fh, source.ParseFull).Parse(ctx)
if err != nil {
return nil, err
Expand All @@ -37,8 +46,10 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
case *ast.ImportSpec:
// For import specs, provide a link to a documentation website, like https://pkg.go.dev.
if target, err := strconv.Unquote(n.Path.Value); err == nil {
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)
}
target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)

// Account for the quotation marks in the positions.
start, end := n.Path.Pos()+1, n.Path.End()-1
if l, err := toProtocolLink(view, m, target, start, end); err == nil {
Expand Down Expand Up @@ -66,6 +77,25 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
return links, nil
}

func moduleAtVersion(ctx context.Context, target string, ph source.PackageHandle) (string, string, bool) {
pkg, err := ph.Check(ctx)
if err != nil {
return "", "", false
}
impPkg, err := pkg.GetImport(target)
if err != nil {
return "", "", false
}
if impPkg.Module() == nil {
return "", "", false
}
version, modpath := impPkg.Module().Version, impPkg.Module().Path
if modpath == "" || version == "" {
return "", "", false
}
return modpath, version, true
}

func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper) []protocol.DocumentLink {
var links []protocol.DocumentLink
for _, index := range view.Options().URLRegexp.FindAllIndex([]byte(src), -1) {
Expand Down
32 changes: 29 additions & 3 deletions internal/lsp/source/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ func (i *IdentifierInfo) linkAndSymbolName() (string, string) {
}
switch obj := obj.(type) {
case *types.PkgName:
return obj.Imported().Path(), obj.Name()
path := obj.Imported().Path()
if mod, version, ok := moduleAtVersion(path, i); ok {
path = strings.Replace(path, mod, mod+"@"+version, 1)
}
return path, obj.Name()
case *types.Builtin:
return fmt.Sprintf("builtin#%s", obj.Name()), obj.Name()
}
Expand Down Expand Up @@ -128,13 +132,35 @@ func (i *IdentifierInfo) linkAndSymbolName() (string, string) {
}
}
}
path := obj.Pkg().Path()
if mod, version, ok := moduleAtVersion(path, i); ok {
path = strings.Replace(path, mod, mod+"@"+version, 1)
}
if rTypeName != "" {
link := fmt.Sprintf("%s#%s.%s", obj.Pkg().Path(), rTypeName, obj.Name())
link := fmt.Sprintf("%s#%s.%s", path, rTypeName, obj.Name())
symbol := fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), rTypeName, obj.Name())
return link, symbol
}
// For most cases, the link is "package/path#symbol".
return fmt.Sprintf("%s#%s", obj.Pkg().Path(), obj.Name()), fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
return fmt.Sprintf("%s#%s", path, obj.Name()), fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
}

func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) {
if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" {
return "", "", false
}
impPkg, err := i.pkg.GetImport(path)
if err != nil {
return "", "", false
}
if impPkg.Module() == nil {
return "", "", false
}
version, modpath := impPkg.Module().Version, impPkg.Module().Path
if modpath == "" || version == "" {
return "", "", false
}
return modpath, version, true
}

// objectString is a wrapper around the types.ObjectString function.
Expand Down
2 changes: 2 additions & 0 deletions internal/lsp/source/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/span"
)

Expand Down Expand Up @@ -359,6 +360,7 @@ type Package interface {
ForTest() string
GetImport(pkgPath string) (Package, error)
Imports() []Package
Module() *packagesinternal.Module
}

type Error struct {
Expand Down
2 changes: 2 additions & 0 deletions internal/lsp/testdata/lsp/primarymod/links/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
_ "database/sql" //@link(`database/sql`, `https://pkg.go.dev/database/sql`)

errors "golang.org/x/xerrors" //@link(`golang.org/x/xerrors`, `https://pkg.go.dev/golang.org/x/xerrors`)

_ "example.com/extramodule/pkg" //@link(`example.com/extramodule/pkg`,`https://pkg.go.dev/example.com/extramodule@v1.0.0/pkg`)
)

var (
Expand Down
2 changes: 1 addition & 1 deletion internal/lsp/testdata/lsp/summary.txt.golden
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ WorkspaceSymbolsCount = 2
FuzzyWorkspaceSymbolsCount = 3
CaseSensitiveWorkspaceSymbolsCount = 2
SignaturesCount = 23
LinksCount = 8
LinksCount = 9
ImplementationsCount = 5

9 changes: 9 additions & 0 deletions internal/lsp/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
Expand Down Expand Up @@ -700,6 +701,14 @@ func Run(t *testing.T, tests Tests, data *Data) {
t.Run("Link", func(t *testing.T) {
t.Helper()
for uri, wantLinks := range data.Links {
// If we are testing GOPATH, then we do not want links with
// the versions attached (pkg.go.dev/repoa/moda@v1.1.0/pkg).
if data.Exported.Exporter == packagestest.GOPATH {
re := regexp.MustCompile(`@(.+?)/`)
for i, link := range wantLinks {
wantLinks[i].Target = re.ReplaceAllString(link.Target, "/")
}
}
t.Run(uriName(uri), func(t *testing.T) {
t.Helper()
tests.Link(t, uri, wantLinks)
Expand Down
23 changes: 23 additions & 0 deletions internal/packagesinternal/packages.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
// Package packagesinternal exposes internal-only fields from go/packages.
package packagesinternal

import "time"

// Fields must match go list;
type Module struct {
Path string // module path
Version string // module version
Versions []string // available module versions (with -versions)
Replace *Module // replaced by this module
Time *time.Time // time version was created
Update *Module // available update, if any (with -u)
Main bool // is this the main module?
Indirect bool // is this module only an indirect dependency of main module?
Dir string // directory holding files for this module, if any
GoMod string // path to go.mod file used when loading this module, if any
GoVersion string // go version used in module
Error *ModuleError // error loading module
}
type ModuleError struct {
Err string // the error itself
}

var GetForTest = func(p interface{}) string { return "" }

var GetModule = func(p interface{}) *Module { return nil }

0 comments on commit 7c4b627

Please sign in to comment.