Skip to content

Commit f01b9f6

Browse files
xieyuschengopherbot
authored andcommitted
gopls/internal/server: support links and hovers for replace directive
This CL supports links and hovers for replace directive. It supports links for local replacement such as 'replace A => ../'. It also respects replacement for a module version(replace A => A v1.2.3), and replacement from a module to another(replace A => B v1.2.3), so the hover messages above import decl in .go files and modules in go.mod are correct. Fixes golang/go#73423 Change-Id: I777ff3b77e399406b066780501c084d59af9e442 Reviewed-on: https://go-review.googlesource.com/c/tools/+/667015 Reviewed-by: Alan Donovan <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> Commit-Queue: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent cd18362 commit f01b9f6

File tree

7 files changed

+234
-27
lines changed

7 files changed

+234
-27
lines changed

gopls/internal/cache/mod.go

+39
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"golang.org/x/mod/modfile"
1515
"golang.org/x/mod/module"
16+
"golang.org/x/tools/go/packages"
1617
"golang.org/x/tools/gopls/internal/file"
1718
"golang.org/x/tools/gopls/internal/label"
1819
"golang.org/x/tools/gopls/internal/protocol"
@@ -25,6 +26,7 @@ import (
2526
type ParsedModule struct {
2627
URI protocol.DocumentURI
2728
File *modfile.File
29+
ReplaceMap map[module.Version]module.Version
2830
Mapper *protocol.Mapper
2931
ParseErrors []*Diagnostic
3032
}
@@ -98,10 +100,19 @@ func parseModImpl(ctx context.Context, fh file.Handle) (*ParsedModule, error) {
98100
})
99101
}
100102
}
103+
104+
replaceMap := make(map[module.Version]module.Version)
105+
if parseErr == nil {
106+
for _, rep := range file.Replace {
107+
replaceMap[rep.Old] = rep.New
108+
}
109+
}
110+
101111
return &ParsedModule{
102112
URI: fh.URI(),
103113
Mapper: m,
104114
File: file,
115+
ReplaceMap: replaceMap,
105116
ParseErrors: parseErrors,
106117
}, parseErr
107118
}
@@ -487,3 +498,31 @@ func findModuleReference(mf *modfile.File, ver module.Version) *modfile.Line {
487498
}
488499
return nil
489500
}
501+
502+
// ResolvedVersion returns the version used for a module, which considers replace directive.
503+
func ResolvedVersion(module *packages.Module) string {
504+
// don't visit replace recursively as src/cmd/go/internal/modinfo/info.go
505+
// visits replace field only once.
506+
if module.Replace != nil {
507+
return module.Replace.Version
508+
}
509+
return module.Version
510+
}
511+
512+
// ResolvedPath returns the the module path, which considers replace directive.
513+
func ResolvedPath(module *packages.Module) string {
514+
if module.Replace != nil {
515+
return module.Replace.Path
516+
}
517+
return module.Path
518+
}
519+
520+
// ResolvedString returns a representation of the Version suitable for logging
521+
// (Path@Version, or just Path if Version is empty),
522+
// which considers replace directive.
523+
func ResolvedString(module *packages.Module) string {
524+
if ResolvedVersion(module) == "" {
525+
ResolvedPath(module)
526+
}
527+
return ResolvedPath(module) + "@" + ResolvedVersion(module)
528+
}

gopls/internal/golang/hover.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
651651
linkPath = ""
652652
} else if linkMeta.Module != nil && linkMeta.Module.Version != "" {
653653
mod := linkMeta.Module
654-
linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1)
654+
linkPath = strings.Replace(linkPath, mod.Path, cache.ResolvedString(mod), 1)
655655
}
656656

657657
var footer string

gopls/internal/mod/hover.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313

1414
"golang.org/x/mod/modfile"
15+
"golang.org/x/mod/module"
1516
"golang.org/x/mod/semver"
1617
"golang.org/x/tools/gopls/internal/cache"
1718
"golang.org/x/tools/gopls/internal/file"
@@ -116,7 +117,7 @@ func hoverOnRequireStatement(ctx context.Context, pm *cache.ParsedModule, offset
116117
options := snapshot.Options()
117118
isPrivate := snapshot.IsGoPrivatePath(req.Mod.Path)
118119
header := formatHeader(req.Mod.Path, options)
119-
explanation = formatExplanation(explanation, req, options, isPrivate)
120+
explanation = formatExplanation(explanation, pm.ReplaceMap, req, options, isPrivate)
120121
vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck)
121122

122123
return &protocol.Hover{
@@ -327,7 +328,7 @@ func vulnerablePkgsInfo(findings []*govulncheck.Finding, useMarkdown bool) strin
327328
return b.String()
328329
}
329330

330-
func formatExplanation(text string, req *modfile.Require, options *settings.Options, isPrivate bool) string {
331+
func formatExplanation(text string, replaceMap map[module.Version]module.Version, req *modfile.Require, options *settings.Options, isPrivate bool) string {
331332
text = strings.TrimSuffix(text, "\n")
332333
splt := strings.Split(text, "\n")
333334
length := len(splt)
@@ -348,7 +349,17 @@ func formatExplanation(text string, req *modfile.Require, options *settings.Opti
348349
if !isPrivate && options.PreferredContentFormat == protocol.Markdown {
349350
target := imp
350351
if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
351-
target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
352+
mod := req.Mod
353+
// respect the repalcement when constructing a module link.
354+
if m, ok := replaceMap[req.Mod]; ok {
355+
// Have: 'replace A v1.2.3 => A vx.x.x' or 'replace A v1.2.3 => B vx.x.x'.
356+
mod = m
357+
} else if m, ok := replaceMap[module.Version{Path: req.Mod.Path}]; ok &&
358+
!modfile.IsDirectoryPath(m.Path) { // exclude local replacement.
359+
// Have: 'replace A => A vx.x.x' or 'replace A => B vx.x.x'.
360+
mod = m
361+
}
362+
target = strings.Replace(target, req.Mod.Path, mod.String(), 1)
352363
}
353364
reference = fmt.Sprintf("[%s](%s)", imp, cache.BuildLink(options.LinkTarget, target, ""))
354365
}

gopls/internal/server/link.go

+41-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import (
1111
"go/ast"
1212
"go/token"
1313
"net/url"
14+
"path/filepath"
1415
"regexp"
1516
"strings"
1617
"sync"
1718

1819
"golang.org/x/mod/modfile"
20+
"golang.org/x/mod/module"
1921
"golang.org/x/tools/gopls/internal/cache"
2022
"golang.org/x/tools/gopls/internal/cache/metadata"
2123
"golang.org/x/tools/gopls/internal/cache/parsego"
@@ -59,6 +61,30 @@ func modLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]
5961
}
6062

6163
var links []protocol.DocumentLink
64+
for _, rep := range pm.File.Replace {
65+
if modfile.IsDirectoryPath(rep.New.Path) {
66+
// Have local replacement, such as 'replace A => ../'.
67+
dep := []byte(rep.New.Path)
68+
start, end := rep.Syntax.Start.Byte, rep.Syntax.End.Byte
69+
i := bytes.Index(pm.Mapper.Content[start:end], dep)
70+
if i < 0 {
71+
continue
72+
}
73+
path := rep.New.Path
74+
if !filepath.IsAbs(path) {
75+
path = filepath.Join(fh.URI().DirPath(), path)
76+
}
77+
// jump to the go.mod file of replaced module.
78+
path = filepath.Join(filepath.Clean(path), "go.mod")
79+
l, err := toProtocolLink(pm.Mapper, protocol.URIFromPath(path).Path(), start+i, start+i+len(dep))
80+
if err != nil {
81+
return nil, err
82+
}
83+
links = append(links, l)
84+
continue
85+
}
86+
}
87+
6288
for _, req := range pm.File.Require {
6389
if req.Syntax == nil {
6490
continue
@@ -73,9 +99,21 @@ func modLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]
7399
if i == -1 {
74100
continue
75101
}
102+
103+
mod := req.Mod
104+
// respect the repalcement when constructing a module link.
105+
if m, ok := pm.ReplaceMap[req.Mod]; ok {
106+
// Have: 'replace A v1.2.3 => A vx.x.x' or 'replace A v1.2.3 => B vx.x.x'.
107+
mod = m
108+
} else if m, ok := pm.ReplaceMap[module.Version{Path: req.Mod.Path}]; ok &&
109+
!modfile.IsDirectoryPath(m.Path) { // exclude local replacement.
110+
// Have: 'replace A => A vx.x.x' or 'replace A => B vx.x.x'.
111+
mod = m
112+
}
113+
76114
// Shift the start position to the location of the
77115
// dependency within the require statement.
78-
target := cache.BuildLink(snapshot.Options().LinkTarget, "mod/"+req.Mod.String(), "")
116+
target := cache.BuildLink(snapshot.Options().LinkTarget, "mod/"+mod.String(), "")
79117
l, err := toProtocolLink(pm.Mapper, target, start+i, start+i+len(dep))
80118
if err != nil {
81119
return nil, err
@@ -142,8 +180,8 @@ func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]p
142180
urlPath := string(importPath)
143181

144182
// For pkg.go.dev, append module version suffix to package import path.
145-
if mp := snapshot.Metadata(depsByImpPath[importPath]); mp != nil && mp.Module != nil && mp.Module.Path != "" && mp.Module.Version != "" {
146-
urlPath = strings.Replace(urlPath, mp.Module.Path, mp.Module.Path+"@"+mp.Module.Version, 1)
183+
if mp := snapshot.Metadata(depsByImpPath[importPath]); mp != nil && mp.Module != nil && cache.ResolvedPath(mp.Module) != "" && cache.ResolvedVersion(mp.Module) != "" {
184+
urlPath = strings.Replace(urlPath, mp.Module.Path, cache.ResolvedString(mp.Module), 1)
147185
}
148186

149187
start, end, err := safetoken.Offsets(pgf.Tok, imp.Path.Pos(), imp.Path.End())

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ package misc
77
import (
88
"testing"
99

10-
. "golang.org/x/tools/gopls/internal/test/integration"
1110
"golang.org/x/tools/gopls/internal/test/compare"
11+
. "golang.org/x/tools/gopls/internal/test/integration"
1212
)
1313

1414
// This is a slight variant of TestHoverOnError in definition_test.go

0 commit comments

Comments
 (0)