gopls: add codelens to reset upgrade diagnostics

This clears the diagnostics that were added by the `Check for
upgrades` codelens for the given go.mod file. The diagnostic
source for the diagnostics from `Check for upgrades` is now
modCheckUpgradesSource instead of modSource in order to allow
us to easily clear these diagnostics.

Fixes golang/go#54065

Change-Id: I7c0824ce1fdfcf2a73ec83342501ceed82fc519f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/426016
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
This commit is contained in:
Suzy Mueller 2022-08-17 17:26:25 -04:00
parent a81fce34b3
commit 7e129ca5ef
12 changed files with 204 additions and 72 deletions

View File

@ -247,6 +247,20 @@ Args:
}
```
### **Reset go.mod diagnostics**
Identifier: `gopls.reset_go_mod_diagnostics`
Reset diagnostics in the go.mod file of a module.
Args:
```
{
// The file URI.
"URI": string,
}
```
### **Run test(s)**
Identifier: `gopls.run_tests`

View File

@ -24,12 +24,12 @@ import (
"golang.org/x/mod/semver"
exec "golang.org/x/sys/execabs"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/bug"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/xcontext"
)
@ -1031,6 +1031,13 @@ func (v *View) RegisterModuleUpgrades(uri span.URI, upgrades map[string]string)
}
}
func (v *View) ClearModuleUpgrades(modfile span.URI) {
v.mu.Lock()
defer v.mu.Unlock()
v.moduleUpgrades[modfile] = nil
}
// 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 {

View File

@ -10,13 +10,13 @@ import (
"sort"
"strings"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/gopls/internal/lsp/mod"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/span"
)
@ -70,14 +70,18 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
switch kind {
case source.Mod:
if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
diags, err := mod.DiagnosticsForMod(ctx, snapshot, fh)
diags, err := mod.ModDiagnostics(ctx, snapshot, fh)
if source.IsNonFatalGoModError(err) {
return nil, nil
}
if err != nil {
return nil, err
}
quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, diags)
udiags, err := mod.ModUpgradeDiagnostics(ctx, snapshot, fh)
if err != nil {
return nil, err
}
quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, append(diags, udiags...))
if err != nil {
return nil, err
}

View File

@ -202,6 +202,22 @@ func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.Dep
return c.GoGetModule(ctx, args)
}
func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, uri command.URIArg) error {
return c.run(ctx, commandConfig{
forURI: uri.URI,
}, func(ctx context.Context, deps commandDeps) error {
deps.snapshot.View().ClearModuleUpgrades(uri.URI.SpanURI())
// Clear all diagnostics coming from the upgrade check source.
// This will clear the diagnostics in all go.mod files, but they
// will be re-calculated when the snapshot is diagnosed again.
c.s.clearDiagnosticSource(modCheckUpgradesSource)
// Re-diagnose the snapshot to remove the diagnostics.
c.s.diagnoseSnapshot(deps.snapshot, nil, false)
return nil
})
}
func (c *commandHandler) GoGetModule(ctx context.Context, args command.DependencyArgs) error {
return c.run(ctx, commandConfig{
progress: "Running go get",

View File

@ -19,28 +19,29 @@ import (
)
const (
AddDependency Command = "add_dependency"
AddImport Command = "add_import"
ApplyFix Command = "apply_fix"
CheckUpgrades Command = "check_upgrades"
EditGoDirective Command = "edit_go_directive"
GCDetails Command = "gc_details"
Generate Command = "generate"
GenerateGoplsMod Command = "generate_gopls_mod"
GoGetPackage Command = "go_get_package"
ListImports Command = "list_imports"
ListKnownPackages Command = "list_known_packages"
RegenerateCgo Command = "regenerate_cgo"
RemoveDependency Command = "remove_dependency"
RunTests Command = "run_tests"
RunVulncheckExp Command = "run_vulncheck_exp"
StartDebugging Command = "start_debugging"
Test Command = "test"
Tidy Command = "tidy"
ToggleGCDetails Command = "toggle_gc_details"
UpdateGoSum Command = "update_go_sum"
UpgradeDependency Command = "upgrade_dependency"
Vendor Command = "vendor"
AddDependency Command = "add_dependency"
AddImport Command = "add_import"
ApplyFix Command = "apply_fix"
CheckUpgrades Command = "check_upgrades"
EditGoDirective Command = "edit_go_directive"
GCDetails Command = "gc_details"
Generate Command = "generate"
GenerateGoplsMod Command = "generate_gopls_mod"
GoGetPackage Command = "go_get_package"
ListImports Command = "list_imports"
ListKnownPackages Command = "list_known_packages"
RegenerateCgo Command = "regenerate_cgo"
RemoveDependency Command = "remove_dependency"
ResetGoModDiagnostics Command = "reset_go_mod_diagnostics"
RunTests Command = "run_tests"
RunVulncheckExp Command = "run_vulncheck_exp"
StartDebugging Command = "start_debugging"
Test Command = "test"
Tidy Command = "tidy"
ToggleGCDetails Command = "toggle_gc_details"
UpdateGoSum Command = "update_go_sum"
UpgradeDependency Command = "upgrade_dependency"
Vendor Command = "vendor"
)
var Commands = []Command{
@ -57,6 +58,7 @@ var Commands = []Command{
ListKnownPackages,
RegenerateCgo,
RemoveDependency,
ResetGoModDiagnostics,
RunTests,
RunVulncheckExp,
StartDebugging,
@ -148,6 +150,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte
return nil, err
}
return nil, s.RemoveDependency(ctx, a0)
case "gopls.reset_go_mod_diagnostics":
var a0 URIArg
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
return nil, err
}
return nil, s.ResetGoModDiagnostics(ctx, a0)
case "gopls.run_tests":
var a0 RunTestsArgs
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@ -364,6 +372,18 @@ func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol
}, nil
}
func NewResetGoModDiagnosticsCommand(title string, a0 URIArg) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {
return protocol.Command{}, err
}
return protocol.Command{
Title: title,
Command: "gopls.reset_go_mod_diagnostics",
Arguments: args,
}, nil
}
func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {

View File

@ -98,6 +98,11 @@ type Interface interface {
// Removes a dependency from the go.mod file of a module.
RemoveDependency(context.Context, RemoveDependencyArgs) error
// ResetGoModDiagnostics: Reset go.mod diagnostics
//
// Reset diagnostics in the go.mod file of a module.
ResetGoModDiagnostics(context.Context, URIArg) error
// GoGetPackage: go get a package
//
// Runs `go get` to fetch a package.

View File

@ -15,14 +15,14 @@ import (
"sync"
"time"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/gopls/internal/lsp/debug/log"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/gopls/internal/lsp/mod"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/template"
"golang.org/x/tools/gopls/internal/lsp/work"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/xcontext"
)
@ -37,6 +37,7 @@ const (
typeCheckSource
orphanedSource
workSource
modCheckUpgradesSource
)
// A diagnosticReport holds results for a single diagnostic source.
@ -93,6 +94,8 @@ func (d diagnosticSource) String() string {
return "FromOrphans"
case workSource:
return "FromGoWork"
case modCheckUpgradesSource:
return "FromCheckForUpgrades"
default:
return fmt.Sprintf("From?%d?", d)
}
@ -238,6 +241,21 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn
}
s.storeDiagnostics(snapshot, id.URI, modSource, diags)
}
upgradeModReports, upgradeErr := mod.UpgradeDiagnostics(ctx, snapshot)
if ctx.Err() != nil {
log.Trace.Log(ctx, "diagnose cancelled")
return
}
if upgradeErr != nil {
event.Error(ctx, "warning: diagnose go.mod upgrades", upgradeErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID()))
}
for id, diags := range upgradeModReports {
if id.URI == "" {
event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename()))
continue
}
s.storeDiagnostics(snapshot, id.URI, modCheckUpgradesSource, diags)
}
// Diagnose the go.work file, if it exists.
workReports, workErr := work.Diagnostics(ctx, snapshot)

View File

@ -63,6 +63,10 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File
if err != nil {
return nil, err
}
reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.URIArg{URI: uri})
if err != nil {
return nil, err
}
// Put the upgrade code lenses above the first require block or statement.
rng, err := firstRequireRange(fh, pm)
if err != nil {
@ -73,6 +77,7 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File
{Range: rng, Command: checkUpgrade},
{Range: rng, Command: upgradeTransitive},
{Range: rng, Command: upgradeDirect},
{Range: rng, Command: reset},
}, nil
}

View File

@ -10,25 +10,36 @@ import (
"context"
"fmt"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
ctx, done := event.Start(ctx, "mod.Diagnostics", tag.Snapshot.Of(snapshot.ID()))
defer done()
reports := map[source.VersionedFileIdentity][]*source.Diagnostic{}
return collectDiagnostics(ctx, snapshot, ModDiagnostics)
}
func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", tag.Snapshot.Of(snapshot.ID()))
defer done()
return collectDiagnostics(ctx, snapshot, ModUpgradeDiagnostics)
}
func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
reports := make(map[source.VersionedFileIdentity][]*source.Diagnostic)
for _, uri := range snapshot.ModFiles() {
fh, err := snapshot.GetVersionedFile(ctx, uri)
if err != nil {
return nil, err
}
reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{}
diagnostics, err := DiagnosticsForMod(ctx, snapshot, fh)
diagnostics, err := diagFn(ctx, snapshot, fh)
if err != nil {
return nil, err
}
@ -43,7 +54,9 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.Vers
return reports, nil
}
func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]*source.Diagnostic, error) {
// ModDiagnostics returns diagnostics from diagnosing the packages in the workspace and
// from tidying the go.mod file.
func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (diagnostics []*source.Diagnostic, err error) {
pm, err := snapshot.ParseMod(ctx, fh)
if err != nil {
if pm == nil || len(pm.ParseErrors) == 0 {
@ -52,40 +65,10 @@ func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source.
return pm.ParseErrors, nil
}
var diagnostics []*source.Diagnostic
// Add upgrade quick fixes for individual modules if we know about them.
upgrades := snapshot.View().ModuleUpgrades(fh.URI())
for _, req := range pm.File.Require {
ver, ok := upgrades[req.Mod.Path]
if !ok || req.Mod.Version == ver {
continue
}
rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
if err != nil {
return nil, err
}
// Upgrade to the exact version we offer the user, not the most recent.
title := fmt.Sprintf("Upgrade to %v", ver)
cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{
URI: protocol.URIFromSpanURI(fh.URI()),
AddRequire: false,
GoCmdArgs: []string{req.Mod.Path + "@" + ver},
})
if err != nil {
return nil, err
}
diagnostics = append(diagnostics, &source.Diagnostic{
URI: fh.URI(),
Range: rng,
Severity: protocol.SeverityInformation,
Source: source.UpgradeNotification,
Message: fmt.Sprintf("%v can be upgraded", req.Mod.Path),
SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
})
}
// Packages in the workspace can contribute diagnostics to go.mod files.
// TODO(rfindley): Try to avoid calling DiagnosePackage on all packages in the workspace here,
// for every go.mod file. If gc_details is enabled, it looks like this could lead to extra
// go command invocations (as gc details is not memoized).
wspkgs, err := snapshot.ActivePackages(ctx)
if err != nil && !source.IsNonFatalGoModError(err) {
event.Error(ctx, fmt.Sprintf("workspace packages: diagnosing %s", pm.URI), err)
@ -114,3 +97,47 @@ func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source.
}
return diagnostics, nil
}
// ModUpgradeDiagnostics adds upgrade quick fixes for individual modules if the upgrades
// are recorded in the view.
func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (upgradeDiagnostics []*source.Diagnostic, err error) {
pm, err := snapshot.ParseMod(ctx, fh)
if err != nil {
if pm == nil || len(pm.ParseErrors) == 0 {
return nil, err
}
return nil, nil
}
upgrades := snapshot.View().ModuleUpgrades(fh.URI())
for _, req := range pm.File.Require {
ver, ok := upgrades[req.Mod.Path]
if !ok || req.Mod.Version == ver {
continue
}
rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
if err != nil {
return nil, err
}
// Upgrade to the exact version we offer the user, not the most recent.
title := fmt.Sprintf("Upgrade to %v", ver)
cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{
URI: protocol.URIFromSpanURI(fh.URI()),
AddRequire: false,
GoCmdArgs: []string{req.Mod.Path + "@" + ver},
})
if err != nil {
return nil, err
}
upgradeDiagnostics = append(upgradeDiagnostics, &source.Diagnostic{
URI: fh.URI(),
Range: rng,
Severity: protocol.SeverityInformation,
Source: source.UpgradeNotification,
Message: fmt.Sprintf("%v can be upgraded", req.Mod.Path),
SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
})
}
return upgradeDiagnostics, nil
}

View File

@ -740,6 +740,12 @@ var GeneratedAPIJSON = &APIJSON{
Doc: "Removes a dependency from the go.mod file of a module.",
ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t\"OnlyDiagnostic\": bool,\n}",
},
{
Command: "gopls.reset_go_mod_diagnostics",
Title: "Reset go.mod diagnostics",
Doc: "Reset diagnostics in the go.mod file of a module.",
ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}",
},
{
Command: "gopls.run_tests",
Title: "Run test(s)",

View File

@ -21,10 +21,10 @@ import (
"golang.org/x/mod/module"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/gopls/internal/lsp/progress"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/span"
)
@ -267,6 +267,9 @@ type View interface {
// required by modfile.
RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string)
// ClearModuleUpgrades clears all upgrades for the modules in modfile.
ClearModuleUpgrades(modfile span.URI)
// FileKind returns the type of a file
FileKind(FileHandle) FileKind
}

View File

@ -217,6 +217,13 @@ require golang.org/x/hello v1.2.3
env.NoDiagnosticAtRegexp("b/go.mod", `require`),
),
)
// Check for upgrades in b/go.mod and then clear them.
env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades)
env.Await(env.DiagnosticAtRegexpWithMessage("b/go.mod", `require`, "can be upgraded"))
env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics)
env.Await(EmptyDiagnostics("b/go.mod"))
// Apply the diagnostics to a/go.mod.
env.ApplyQuickFixes("a/go.mod", d.Diagnostics)
env.Await(env.DoneWithChangeWatchedFiles())
if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA {