Skip to content

Commit 98b3097

Browse files
committed
internal/lsp: add codelens for go.mod dependency upgrades
This change adds a code lens for go.mod files that will let a user know if a module can be upgraded, once it is clicked gopls will run a command to update that module. Updates golang/go#36501 Change-Id: Id22b8097ede4972cf73bc029ec927544a71b7150 Reviewed-on: https://go-review.googlesource.com/c/tools/+/218557 Run-TryBot: Rohan Challa <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent 88e652f commit 98b3097

File tree

21 files changed

+443
-16
lines changed

21 files changed

+443
-16
lines changed

internal/lsp/cache/mod_tidy.go renamed to internal/lsp/cache/mod.go

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,24 @@ import (
2525
errors "golang.org/x/xerrors"
2626
)
2727

28-
const ModTidyError = "go mod tidy"
29-
const SyntaxError = "syntax"
28+
const (
29+
ModTidyError = "go mod tidy"
30+
SyntaxError = "syntax"
31+
)
3032

3133
type parseModKey struct {
34+
view string
35+
gomod string
36+
cfg string
37+
}
38+
39+
type parseModHandle struct {
40+
handle *memoize.Handle
41+
file source.FileHandle
42+
cfg *packages.Config
43+
}
44+
45+
type modTidyKey struct {
3246
view string
3347
imports string
3448
gomod string
@@ -66,6 +80,9 @@ type modTidyData struct {
6680
// removing the ones that are identical in the original and ideal go.mods.
6781
missingDeps map[string]*modfile.Require
6882

83+
// upgrades is a map of path->version that contains any upgrades for the go.mod.
84+
upgrades map[string]string
85+
6986
// parseErrors are the errors that arise when we diff between a user's go.mod
7087
// and the "tidied" go.mod.
7188
parseErrors []source.Error
@@ -74,6 +91,119 @@ type modTidyData struct {
7491
err error
7592
}
7693

94+
func (pmh *parseModHandle) String() string {
95+
return pmh.File().Identity().URI.Filename()
96+
}
97+
98+
func (pmh *parseModHandle) File() source.FileHandle {
99+
return pmh.file
100+
}
101+
102+
func (pmh *parseModHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
103+
v := pmh.handle.Get(ctx)
104+
if v == nil {
105+
return nil, nil, nil, errors.Errorf("no parsed file for %s", pmh.File().Identity().URI)
106+
}
107+
data := v.(*modTidyData)
108+
return data.origParsedFile, data.origMapper, data.upgrades, data.err
109+
}
110+
111+
func (s *snapshot) ParseModHandle(ctx context.Context) (source.ParseModHandle, error) {
112+
cfg := s.Config(ctx)
113+
folder := s.View().Folder().Filename()
114+
115+
realURI, tempURI := s.view.ModFiles()
116+
fh, err := s.GetFile(realURI)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
key := parseModKey{
122+
view: folder,
123+
gomod: fh.Identity().String(),
124+
cfg: hashConfig(cfg),
125+
}
126+
h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
127+
data := &modTidyData{}
128+
129+
uri := fh.Identity().URI
130+
131+
ctx, done := trace.StartSpan(ctx, "cache.ParseModHandle", telemetry.File.Of(uri))
132+
defer done()
133+
134+
contents, _, err := fh.Read(ctx)
135+
if err != nil {
136+
data.err = err
137+
return data
138+
}
139+
// If the filehandle passed in is equal to the view's go.mod file, and we have
140+
// a tempModfile, copy the real go.mod file content into the temp go.mod file.
141+
if realURI == uri && tempURI != "" {
142+
if err := ioutil.WriteFile(tempURI.Filename(), contents, os.ModePerm); err != nil {
143+
data.err = err
144+
return data
145+
}
146+
}
147+
mapper := &protocol.ColumnMapper{
148+
URI: uri,
149+
Converter: span.NewContentConverter(uri.Filename(), contents),
150+
Content: contents,
151+
}
152+
parsedFile, err := modfile.Parse(uri.Filename(), contents, nil)
153+
if err != nil {
154+
data.err = err
155+
return data
156+
}
157+
data = &modTidyData{
158+
origfh: fh,
159+
origParsedFile: parsedFile,
160+
origMapper: mapper,
161+
}
162+
// Only check if any dependencies can be upgraded if the passed in
163+
// go.mod file is equal to the view's go.mod file.
164+
if realURI == uri {
165+
data.upgrades, data.err = dependencyUpgrades(ctx, cfg, folder, data)
166+
}
167+
return data
168+
})
169+
return &parseModHandle{
170+
handle: h,
171+
file: fh,
172+
cfg: cfg,
173+
}, nil
174+
}
175+
176+
func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string, modData *modTidyData) (map[string]string, error) {
177+
if len(modData.origParsedFile.Require) == 0 {
178+
return nil, nil
179+
}
180+
// Run "go list -u -m all" to be able to see which deps can be upgraded.
181+
args := []string{"list"}
182+
args = append(args, cfg.BuildFlags...)
183+
args = append(args, []string{"-u", "-m", "all"}...)
184+
stdout, err := source.InvokeGo(ctx, folder, cfg.Env, args...)
185+
if err != nil {
186+
return nil, err
187+
}
188+
upgradesList := strings.Split(stdout.String(), "\n")
189+
if len(upgradesList) <= 1 {
190+
return nil, nil
191+
}
192+
upgrades := make(map[string]string)
193+
for _, upgrade := range upgradesList[1:] {
194+
// Example: "github.com/x/tools v1.1.0 [v1.2.0]"
195+
info := strings.Split(upgrade, " ")
196+
if len(info) < 3 {
197+
continue
198+
}
199+
dep, version := info[0], info[2]
200+
latest := version[1:] // remove the "["
201+
latest = strings.TrimSuffix(latest, "]") // remove the "]"
202+
upgrades[dep] = latest
203+
}
204+
return upgrades, nil
205+
}
206+
77207
func (mth *modTidyHandle) String() string {
78208
return mth.File().Identity().URI.Filename()
79209
}
@@ -108,7 +238,7 @@ func (s *snapshot) ModTidyHandle(ctx context.Context, realfh source.FileHandle)
108238
if err != nil {
109239
return nil, err
110240
}
111-
key := parseModKey{
241+
key := modTidyKey{
112242
view: folder,
113243
imports: imports,
114244
gomod: realfh.Identity().Identifier,

internal/lsp/cmd/test/cmdtest.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Con
7070
return r
7171
}
7272

73+
func (r *runner) CodeLens(t *testing.T, spn span.Span, want []protocol.CodeLens) {
74+
//TODO: add command line completions tests when it works
75+
}
76+
7377
func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
7478
//TODO: add command line completions tests when it works
7579
}

internal/lsp/command.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,23 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
3030
return nil, errors.Errorf("%s is not a mod file", uri)
3131
}
3232
// Run go.mod tidy on the view.
33-
// TODO: This should go through the ModTidyHandle on the view.
34-
// That will also allow us to move source.InvokeGo into internal/lsp/cache.
3533
if _, err := source.InvokeGo(ctx, view.Folder().Filename(), snapshot.Config(ctx).Env, "mod", "tidy"); err != nil {
3634
return nil, err
3735
}
36+
case "upgrade.dependency":
37+
if len(params.Arguments) < 2 {
38+
return nil, errors.Errorf("expected one file URI and one dependency for call to `go get`, got %v", params.Arguments)
39+
}
40+
uri := span.NewURI(params.Arguments[0].(string))
41+
view, err := s.session.ViewOf(uri)
42+
if err != nil {
43+
return nil, err
44+
}
45+
dep := params.Arguments[1].(string)
46+
// Run "go get" on the dependency to upgrade it to the latest version.
47+
if _, err := source.InvokeGo(ctx, view.Folder().Filename(), view.Snapshot().Config(ctx).Env, "get", dep); err != nil {
48+
return nil, err
49+
}
3850
}
3951
return nil, nil
4052
}

internal/lsp/lsp_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"golang.org/x/tools/go/packages/packagestest"
1919
"golang.org/x/tools/internal/lsp/cache"
2020
"golang.org/x/tools/internal/lsp/diff"
21+
"golang.org/x/tools/internal/lsp/mod"
2122
"golang.org/x/tools/internal/lsp/protocol"
2223
"golang.org/x/tools/internal/lsp/source"
2324
"golang.org/x/tools/internal/lsp/tests"
@@ -98,6 +99,23 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
9899
}
99100
}
100101

102+
func (r *runner) CodeLens(t *testing.T, spn span.Span, want []protocol.CodeLens) {
103+
if source.DetectLanguage("", spn.URI().Filename()) != source.Mod {
104+
return
105+
}
106+
v, err := r.server.session.ViewOf(spn.URI())
107+
if err != nil {
108+
t.Fatal(err)
109+
}
110+
got, err := mod.CodeLens(r.ctx, v.Snapshot(), spn.URI())
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
if diff := tests.DiffCodeLens(spn.URI(), want, got); diff != "" {
115+
t.Error(diff)
116+
}
117+
}
118+
101119
func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []source.Diagnostic) {
102120
// Get the diagnostics for this view if we have not done it before.
103121
if r.diagnostics == nil {

internal/lsp/mod/code_lens.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package mod
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"golang.org/x/tools/internal/lsp/protocol"
8+
"golang.org/x/tools/internal/lsp/source"
9+
"golang.org/x/tools/internal/lsp/telemetry"
10+
"golang.org/x/tools/internal/span"
11+
"golang.org/x/tools/internal/telemetry/trace"
12+
)
13+
14+
func CodeLens(ctx context.Context, snapshot source.Snapshot, uri span.URI) ([]protocol.CodeLens, error) {
15+
realURI, _ := snapshot.View().ModFiles()
16+
// Check the case when the tempModfile flag is turned off.
17+
if realURI == "" {
18+
return nil, nil
19+
}
20+
// Only get code lens on the go.mod for the view.
21+
if uri != realURI {
22+
return nil, nil
23+
}
24+
ctx, done := trace.StartSpan(ctx, "mod.CodeLens", telemetry.File.Of(realURI))
25+
defer done()
26+
27+
pmh, err := snapshot.ParseModHandle(ctx)
28+
if err != nil {
29+
return nil, err
30+
}
31+
f, m, upgrades, err := pmh.Upgrades(ctx)
32+
var codelens []protocol.CodeLens
33+
for _, req := range f.Require {
34+
dep := req.Mod.Path
35+
latest, ok := upgrades[dep]
36+
if !ok {
37+
continue
38+
}
39+
// Get the range of the require directive.
40+
s, e := req.Syntax.Start, req.Syntax.End
41+
line, col, err := m.Converter.ToPosition(s.Byte)
42+
if err != nil {
43+
return nil, err
44+
}
45+
start := span.NewPoint(line, col, s.Byte)
46+
line, col, err = m.Converter.ToPosition(e.Byte)
47+
if err != nil {
48+
return nil, err
49+
}
50+
end := span.NewPoint(line, col, e.Byte)
51+
rng, err := m.Range(span.New(uri, start, end))
52+
if err != nil {
53+
return nil, err
54+
}
55+
codelens = append(codelens, protocol.CodeLens{
56+
Range: rng,
57+
Command: protocol.Command{
58+
Title: fmt.Sprintf("Upgrade dependency to %s", latest),
59+
Command: "upgrade.dependency",
60+
Arguments: []interface{}{uri, dep},
61+
},
62+
})
63+
}
64+
return codelens, err
65+
}

internal/lsp/server.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"sync"
1111

1212
"golang.org/x/tools/internal/jsonrpc2"
13+
"golang.org/x/tools/internal/lsp/mod"
1314
"golang.org/x/tools/internal/lsp/protocol"
1415
"golang.org/x/tools/internal/lsp/source"
1516
"golang.org/x/tools/internal/span"
@@ -69,6 +70,22 @@ func (s *Server) cancelRequest(ctx context.Context, params *protocol.CancelParam
6970
}
7071

7172
func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
73+
uri := span.NewURI(params.TextDocument.URI)
74+
view, err := s.session.ViewOf(uri)
75+
if err != nil {
76+
return nil, err
77+
}
78+
snapshot := view.Snapshot()
79+
fh, err := snapshot.GetFile(uri)
80+
if err != nil {
81+
return nil, err
82+
}
83+
switch fh.Identity().Kind {
84+
case source.Go:
85+
return nil, nil
86+
case source.Mod:
87+
return mod.CodeLens(ctx, snapshot, uri)
88+
}
7289
return nil, nil
7390
}
7491

internal/lsp/source/options.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ func DefaultOptions() Options {
6666
Sum: {},
6767
},
6868
SupportedCommands: []string{
69-
"tidy", // for go.mod files
69+
"tidy", // for go.mod files
70+
"upgrade.dependency", // for go.mod dependency upgrades
7071
},
7172
},
7273
UserOptions: UserOptions{

internal/lsp/source/source_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,10 @@ func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {
894894
// This is a pure LSP feature, no source level functionality to be tested.
895895
}
896896

897+
func (r *runner) CodeLens(t *testing.T, spn span.Span, want []protocol.CodeLens) {
898+
// This is a pure LSP feature, no source level functionality to be tested.
899+
}
900+
897901
func spanToRange(data *tests.Data, spn span.Span) (*protocol.ColumnMapper, protocol.Range, error) {
898902
m, err := data.Mapper(spn.URI())
899903
if err != nil {

internal/lsp/source/view.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ type Snapshot interface {
4848
// This function can have no data or error if there is no modfile detected.
4949
ModTidyHandle(ctx context.Context, fh FileHandle) (ModTidyHandle, error)
5050

51+
// ParseModHandle returns a ParseModHandle for the view's go.mod file handle.
52+
// This function can have no data or error if there is no modfile detected.
53+
ParseModHandle(ctx context.Context) (ParseModHandle, error)
54+
5155
// PackageHandles returns the PackageHandles for the packages that this file
5256
// belongs to.
5357
PackageHandles(ctx context.Context, fh FileHandle) ([]PackageHandle, error)
@@ -254,6 +258,16 @@ type ParseGoHandle interface {
254258
Cached() (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error)
255259
}
256260

261+
// ParseModHandle represents a handle to the modfile for a go.mod.
262+
type ParseModHandle interface {
263+
// File returns a file handle for which to get the modfile.
264+
File() FileHandle
265+
266+
// Upgrades returns the parsed modfile, a mapper, and any dependency upgrades
267+
// for the go.mod file. If the file is not available, returns nil and an error.
268+
Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
269+
}
270+
257271
// ModTidyHandle represents a handle to the modfile for a go.mod.
258272
type ModTidyHandle interface {
259273
// File returns a file handle for which to get the modfile.

internal/lsp/testdata/indirect/summary.txt.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
-- summary --
2+
CodeLensCount = 0
23
CompletionsCount = 0
34
CompletionSnippetCount = 0
45
UnimportedCompletionsCount = 0

internal/lsp/testdata/lsp/summary.txt.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
-- summary --
2+
CodeLensCount = 0
23
CompletionsCount = 228
34
CompletionSnippetCount = 67
45
UnimportedCompletionsCount = 11

internal/lsp/testdata/missingdep/summary.txt.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
-- summary --
2+
CodeLensCount = 0
23
CompletionsCount = 0
34
CompletionSnippetCount = 0
45
UnimportedCompletionsCount = 0

0 commit comments

Comments
 (0)