Skip to content

Commit

Permalink
internal/lsp/mod: add an "Upgrade direct dependencies" code lens
Browse files Browse the repository at this point in the history
`go get -u all` can have unexpected behavior for some users, since
upgrades all transitive dependencies as well as direct ones. Offer users
the ability to upgrade only direct dependencies via a code lens.

Also, change the placement of the upgrade code lenses. The module
declaration was getting cluttered, and they make more sense above the
requires anyway.

Updates golang/go#38339

Change-Id: I6790a02f2a334f3f4d1d89934b2c4dc4f11845ce
Reviewed-on: https://go-review.googlesource.com/c/tools/+/275432
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
  • Loading branch information
stamblerre authored and marwan-at-work committed Dec 18, 2020
1 parent c4e00c9 commit e0f7517
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 30 deletions.
47 changes: 36 additions & 11 deletions gopls/internal/regtest/codelens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const (
// dependencies in a go.mod file. It checks for the code lens that suggests
// an update and then executes the command associated with that code lens. A
// regression test for golang/go#39446.
func TestUpdateCodelens(t *testing.T) {
func TestUpgradeCodelens(t *testing.T) {
const proxyWithLatest = `
-- golang.org/x/hello@v1.3.3/go.mod --
module golang.org/x/hello
Expand Down Expand Up @@ -111,21 +111,46 @@ func main() {
_ = hi.Goodbye
}
`
runner.Run(t, shouldUpdateDep, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
env.ExecuteCodeLensCommand("go.mod", source.CommandUpgradeDependency)
env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
got := env.Editor.BufferText("go.mod")
const wantGoMod = `module mod.com
for _, commandTitle := range []string{
"Upgrade transitive dependencies",
"Upgrade direct dependencies",
} {
t.Run(commandTitle, func(t *testing.T) {
withOptions(
WithProxyFiles(proxyWithLatest),
).run(t, shouldUpdateDep, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
var lens protocol.CodeLens
var found bool
for _, l := range env.CodeLens("go.mod") {
if l.Command.Title == commandTitle {
lens = l
found = true
}
}
if !found {
t.Fatalf("found no command with the title %s", commandTitle)
}
if _, err := env.Editor.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{
Command: lens.Command.Command,
Arguments: lens.Command.Arguments,
}); err != nil {
t.Fatal(err)
}
env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
got := env.Editor.BufferText("go.mod")
const wantGoMod = `module mod.com
go 1.12
require golang.org/x/hello v1.3.3
`
if got != wantGoMod {
t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(wantGoMod, got))
}
}, WithProxyFiles(proxyWithLatest))
if got != wantGoMod {
t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(wantGoMod, got))
}
})
})
}
}

func TestUnusedDependenciesCodelens(t *testing.T) {
Expand Down
79 changes: 60 additions & 19 deletions internal/lsp/mod/code_lens.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"

"golang.org/x/mod/modfile"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
Expand All @@ -14,13 +15,13 @@ import (
// LensFuncs returns the supported lensFuncs for go.mod files.
func LensFuncs() map[string]source.LensFunc {
return map[string]source.LensFunc{
source.CommandUpgradeDependency.Name: upgradeLens,
source.CommandUpgradeDependency.Name: upgradeLenses,
source.CommandTidy.Name: tidyLens,
source.CommandVendor.Name: vendorLens,
}
}

func upgradeLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
pm, err := snapshot.ParseMod(ctx, fh)
if err != nil || pm.File == nil {
return nil, err
Expand All @@ -29,22 +30,41 @@ func upgradeLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHa
// Nothing to upgrade.
return nil, nil
}
upgradeDepArgs, err := source.MarshalArgs(fh.URI(), false, []string{"-u", "all"})
upgradeTransitiveArgs, err := source.MarshalArgs(fh.URI(), false, []string{"-u", "all"})
if err != nil {
return nil, err
}
rng, err := moduleStmtRange(fh, pm)
var requires []string
for _, req := range pm.File.Require {
requires = append(requires, req.Mod.Path)
}
upgradeDirectArgs, err := source.MarshalArgs(fh.URI(), false, requires)
if err != nil {
return nil, err
}
return []protocol.CodeLens{{
Range: rng,
Command: protocol.Command{
Title: "Upgrade all dependencies",
Command: source.CommandUpgradeDependency.ID(),
Arguments: upgradeDepArgs,
// Put the upgrade code lenses above the first require block or statement.
rng, err := firstRequireRange(fh, pm)
if err != nil {
return nil, err
}
return []protocol.CodeLens{
{
Range: rng,
Command: protocol.Command{
Title: "Upgrade transitive dependencies",
Command: source.CommandUpgradeDependency.ID(),
Arguments: upgradeTransitiveArgs,
},
},
}}, nil
{
Range: rng,
Command: protocol.Command{
Title: "Upgrade direct dependencies",
Command: source.CommandUpgradeDependency.ID(),
Arguments: upgradeDirectArgs,
},
},
}, nil

}

Expand Down Expand Up @@ -110,19 +130,40 @@ func moduleStmtRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Ra
return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI())
}
syntax := pm.File.Module.Syntax
line, col, err := pm.Mapper.Converter.ToPosition(syntax.Start.Byte)
if err != nil {
return protocol.Range{}, err
return lineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End)
}

// firstRequireRange returns the range for the first "require" in the given
// go.mod file. This is either a require block or an individual require line.
func firstRequireRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Range, error) {
if len(pm.File.Require) == 0 {
return protocol.Range{}, fmt.Errorf("no requires in the file %s", fh.URI())
}
start := span.NewPoint(line, col, syntax.Start.Byte)
line, col, err = pm.Mapper.Converter.ToPosition(syntax.End.Byte)
var start, end modfile.Position
for _, stmt := range pm.File.Syntax.Stmt {
if b, ok := stmt.(*modfile.LineBlock); ok && len(b.Token) == 1 && b.Token[0] == "require" {
start, end = b.Span()
break
}
}

firstRequire := pm.File.Require[0].Syntax
if firstRequire.Start.Byte < start.Byte {
start, end = firstRequire.Start, firstRequire.End
}
return lineToRange(pm.Mapper, fh.URI(), start, end)
}

func lineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) {
line, col, err := m.Converter.ToPosition(start.Byte)
if err != nil {
return protocol.Range{}, err
}
end := span.NewPoint(line, col, syntax.End.Byte)
rng, err := pm.Mapper.Range(span.New(fh.URI(), start, end))
s := span.NewPoint(line, col, start.Byte)
line, col, err = m.Converter.ToPosition(end.Byte)
if err != nil {
return protocol.Range{}, err
}
return rng, err
e := span.NewPoint(line, col, end.Byte)
return m.Range(span.New(uri, s, e))
}

0 comments on commit e0f7517

Please sign in to comment.