diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index c202b51a2c..c942be48fd 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -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` diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index cc88c3de5a..025419d71d 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -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 { diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index b09f42214c..1d062d717e 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -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 } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index f01bcb1a23..811307c1ff 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -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", diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go index 301cf6f604..615f9509c8 100644 --- a/gopls/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -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 { diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index 1fff896bd1..d49a4330d8 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -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. diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index d7587621a6..c00eb4206e 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -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) diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go index 2068e60df4..daafee0cb5 100644 --- a/gopls/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -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 } diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 96b95c059d..1efbfdd7de 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -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 +} diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 56f7c0f552..62315079a0 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -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)", diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 8bf0344e7b..47f463e13b 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -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 } diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index a0ddfe6248..096d572f90 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -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 {