aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/command.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/command.go')
-rw-r--r--gopls/internal/lsp/command.go964
1 files changed, 964 insertions, 0 deletions
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
new file mode 100644
index 000000000..75e5ef8b5
--- /dev/null
+++ b/gopls/internal/lsp/command.go
@@ -0,0 +1,964 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package lsp
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strings"
+ "time"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/govulncheck"
+ "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/lsp/debug"
+ "golang.org/x/tools/gopls/internal/lsp/progress"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/gopls/internal/vulncheck"
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/gocommand"
+ "golang.org/x/tools/internal/xcontext"
+)
+
+func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
+ var found bool
+ for _, name := range s.session.Options().SupportedCommands {
+ if name == params.Command {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return nil, fmt.Errorf("%s is not a supported command", params.Command)
+ }
+
+ handler := &commandHandler{
+ s: s,
+ params: params,
+ }
+ return command.Dispatch(ctx, params, handler)
+}
+
+type commandHandler struct {
+ s *Server
+ params *protocol.ExecuteCommandParams
+}
+
+// commandConfig configures common command set-up and execution.
+type commandConfig struct {
+ async bool // whether to run the command asynchronously. Async commands can only return errors.
+ requireSave bool // whether all files must be saved for the command to work
+ progress string // title to use for progress reporting. If empty, no progress will be reported.
+ forURI protocol.DocumentURI // URI to resolve to a snapshot. If unset, snapshot will be nil.
+}
+
+// commandDeps is evaluated from a commandConfig. Note that not all fields may
+// be populated, depending on which configuration is set. See comments in-line
+// for details.
+type commandDeps struct {
+ snapshot source.Snapshot // present if cfg.forURI was set
+ fh source.FileHandle // present if cfg.forURI was set
+ work *progress.WorkDone // present cfg.progress was set
+}
+
+type commandFunc func(context.Context, commandDeps) error
+
+// run performs command setup for command execution, and invokes the given run
+// function. If cfg.async is set, run executes the given func in a separate
+// goroutine, and returns as soon as setup is complete and the goroutine is
+// scheduled.
+//
+// Invariant: if the resulting error is non-nil, the given run func will
+// (eventually) be executed exactly once.
+func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run commandFunc) (err error) {
+ if cfg.requireSave {
+ var unsaved []string
+ for _, overlay := range c.s.session.Overlays() {
+ if !overlay.Saved() {
+ unsaved = append(unsaved, overlay.URI().Filename())
+ }
+ }
+ if len(unsaved) > 0 {
+ return fmt.Errorf("All files must be saved first (unsaved: %v).", unsaved)
+ }
+ }
+ var deps commandDeps
+ if cfg.forURI != "" {
+ var ok bool
+ var release func()
+ deps.snapshot, deps.fh, ok, release, err = c.s.beginFileRequest(ctx, cfg.forURI, source.UnknownKind)
+ defer release()
+ if !ok {
+ if err != nil {
+ return err
+ }
+ return fmt.Errorf("invalid file URL: %v", cfg.forURI)
+ }
+ }
+ ctx, cancel := context.WithCancel(xcontext.Detach(ctx))
+ if cfg.progress != "" {
+ deps.work = c.s.progress.Start(ctx, cfg.progress, "Running...", c.params.WorkDoneToken, cancel)
+ }
+ runcmd := func() error {
+ defer cancel()
+ err := run(ctx, deps)
+ if deps.work != nil {
+ switch {
+ case errors.Is(err, context.Canceled):
+ deps.work.End(ctx, "canceled")
+ case err != nil:
+ event.Error(ctx, "command error", err)
+ deps.work.End(ctx, "failed")
+ default:
+ deps.work.End(ctx, "completed")
+ }
+ }
+ return err
+ }
+ if cfg.async {
+ go func() {
+ if err := runcmd(); err != nil {
+ if showMessageErr := c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+ Type: protocol.Error,
+ Message: err.Error(),
+ }); showMessageErr != nil {
+ event.Error(ctx, fmt.Sprintf("failed to show message: %q", err.Error()), showMessageErr)
+ }
+ }
+ }()
+ return nil
+ }
+ return runcmd()
+}
+
+func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs) error {
+ return c.run(ctx, commandConfig{
+ // Note: no progress here. Applying fixes should be quick.
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ edits, err := source.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Range)
+ if err != nil {
+ return err
+ }
+ var changes []protocol.DocumentChanges
+ for _, edit := range edits {
+ edit := edit
+ changes = append(changes, protocol.DocumentChanges{
+ TextDocumentEdit: &edit,
+ })
+ }
+ r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: changes,
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if !r.Applied {
+ return errors.New(r.FailureReason)
+ }
+ return nil
+ })
+}
+
+func (c *commandHandler) RegenerateCgo(ctx context.Context, args command.URIArg) error {
+ return c.run(ctx, commandConfig{
+ progress: "Regenerating Cgo",
+ }, func(ctx context.Context, deps commandDeps) error {
+ mod := source.FileModification{
+ URI: args.URI.SpanURI(),
+ Action: source.InvalidateMetadata,
+ }
+ return c.s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo)
+ })
+}
+
+func (c *commandHandler) CheckUpgrades(ctx context.Context, args command.CheckUpgradesArgs) error {
+ return c.run(ctx, commandConfig{
+ forURI: args.URI,
+ progress: "Checking for upgrades",
+ }, func(ctx context.Context, deps commandDeps) error {
+ upgrades, err := c.s.getUpgrades(ctx, deps.snapshot, args.URI.SpanURI(), args.Modules)
+ if err != nil {
+ return err
+ }
+ deps.snapshot.View().RegisterModuleUpgrades(args.URI.SpanURI(), upgrades)
+ // Re-diagnose the snapshot to publish the new module diagnostics.
+ c.s.diagnoseSnapshot(deps.snapshot, nil, false)
+ return nil
+ })
+}
+
+func (c *commandHandler) AddDependency(ctx context.Context, args command.DependencyArgs) error {
+ return c.GoGetModule(ctx, args)
+}
+
+func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.DependencyArgs) error {
+ return c.GoGetModule(ctx, args)
+}
+
+func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, args command.ResetGoModDiagnosticsArgs) error {
+ return c.run(ctx, commandConfig{
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ // Clear all diagnostics coming from the upgrade check source and vulncheck.
+ // This will clear the diagnostics in all go.mod files, but they
+ // will be re-calculated when the snapshot is diagnosed again.
+ if args.DiagnosticSource == "" || args.DiagnosticSource == string(source.UpgradeNotification) {
+ deps.snapshot.View().ClearModuleUpgrades(args.URI.SpanURI())
+ c.s.clearDiagnosticSource(modCheckUpgradesSource)
+ }
+
+ if args.DiagnosticSource == "" || args.DiagnosticSource == string(source.Govulncheck) {
+ deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), nil)
+ c.s.clearDiagnosticSource(modVulncheckSource)
+ }
+
+ // 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",
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
+ return runGoGetModule(invoke, args.AddRequire, args.GoCmdArgs)
+ })
+ })
+}
+
+// TODO(rFindley): UpdateGoSum, Tidy, and Vendor could probably all be one command.
+func (c *commandHandler) UpdateGoSum(ctx context.Context, args command.URIArgs) error {
+ return c.run(ctx, commandConfig{
+ progress: "Updating go.sum",
+ }, func(ctx context.Context, deps commandDeps) error {
+ for _, uri := range args.URIs {
+ snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind)
+ defer release()
+ if !ok {
+ return err
+ }
+ if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
+ _, err := invoke("list", "all")
+ return err
+ }); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+func (c *commandHandler) Tidy(ctx context.Context, args command.URIArgs) error {
+ return c.run(ctx, commandConfig{
+ requireSave: true,
+ progress: "Running go mod tidy",
+ }, func(ctx context.Context, deps commandDeps) error {
+ for _, uri := range args.URIs {
+ snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind)
+ defer release()
+ if !ok {
+ return err
+ }
+ if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
+ _, err := invoke("mod", "tidy")
+ return err
+ }); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+func (c *commandHandler) Vendor(ctx context.Context, args command.URIArg) error {
+ return c.run(ctx, commandConfig{
+ requireSave: true,
+ progress: "Running go mod vendor",
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ // Use RunGoCommandPiped here so that we don't compete with any other go
+ // command invocations. go mod vendor deletes modules.txt before recreating
+ // it, and therefore can run into file locking issues on Windows if that
+ // file is in use by another process, such as go list.
+ //
+ // If golang/go#44119 is resolved, go mod vendor will instead modify
+ // modules.txt in-place. In that case we could theoretically allow this
+ // command to run concurrently.
+ err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal|source.AllowNetwork, &gocommand.Invocation{
+ Verb: "mod",
+ Args: []string{"vendor"},
+ WorkingDir: filepath.Dir(args.URI.SpanURI().Filename()),
+ }, &bytes.Buffer{}, &bytes.Buffer{})
+ return err
+ })
+}
+
+func (c *commandHandler) EditGoDirective(ctx context.Context, args command.EditGoDirectiveArgs) error {
+ return c.run(ctx, commandConfig{
+ requireSave: true, // if go.mod isn't saved it could cause a problem
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, args.URI, source.UnknownKind)
+ defer release()
+ if !ok {
+ return err
+ }
+ if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
+ _, err := invoke("mod", "edit", "-go", args.Version)
+ return err
+ }); err != nil {
+ return err
+ }
+ return nil
+ })
+}
+
+func (c *commandHandler) RemoveDependency(ctx context.Context, args command.RemoveDependencyArgs) error {
+ return c.run(ctx, commandConfig{
+ progress: "Removing dependency",
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ // If the module is tidied apart from the one unused diagnostic, we can
+ // run `go get module@none`, and then run `go mod tidy`. Otherwise, we
+ // must make textual edits.
+ // TODO(rstambler): In Go 1.17+, we will be able to use the go command
+ // without checking if the module is tidy.
+ if args.OnlyDiagnostic {
+ return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
+ if err := runGoGetModule(invoke, false, []string{args.ModulePath + "@none"}); err != nil {
+ return err
+ }
+ _, err := invoke("mod", "tidy")
+ return err
+ })
+ }
+ pm, err := deps.snapshot.ParseMod(ctx, deps.fh)
+ if err != nil {
+ return err
+ }
+ edits, err := dropDependency(deps.snapshot, pm, args.ModulePath)
+ if err != nil {
+ return err
+ }
+ response, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: []protocol.DocumentChanges{
+ {
+ TextDocumentEdit: &protocol.TextDocumentEdit{
+ TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
+ Version: deps.fh.Version(),
+ TextDocumentIdentifier: protocol.TextDocumentIdentifier{
+ URI: protocol.URIFromSpanURI(deps.fh.URI()),
+ },
+ },
+ Edits: edits,
+ },
+ },
+ },
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if !response.Applied {
+ return fmt.Errorf("edits not applied because of %s", response.FailureReason)
+ }
+ return nil
+ })
+}
+
+// dropDependency returns the edits to remove the given require from the go.mod
+// file.
+func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePath string) ([]protocol.TextEdit, error) {
+ // We need a private copy of the parsed go.mod file, since we're going to
+ // modify it.
+ copied, err := modfile.Parse("", pm.Mapper.Content, nil)
+ if err != nil {
+ return nil, err
+ }
+ if err := copied.DropRequire(modulePath); err != nil {
+ return nil, err
+ }
+ copied.Cleanup()
+ newContent, err := copied.Format()
+ if err != nil {
+ return nil, err
+ }
+ // Calculate the edits to be made due to the change.
+ diff := snapshot.View().Options().ComputeEdits(string(pm.Mapper.Content), string(newContent))
+ return source.ToProtocolEdits(pm.Mapper, diff)
+}
+
+func (c *commandHandler) Test(ctx context.Context, uri protocol.DocumentURI, tests, benchmarks []string) error {
+ return c.RunTests(ctx, command.RunTestsArgs{
+ URI: uri,
+ Tests: tests,
+ Benchmarks: benchmarks,
+ })
+}
+
+func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs) error {
+ return c.run(ctx, commandConfig{
+ async: true,
+ progress: "Running go test",
+ requireSave: true,
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ if err := c.runTests(ctx, deps.snapshot, deps.work, args.URI, args.Tests, args.Benchmarks); err != nil {
+ return fmt.Errorf("running tests failed: %w", err)
+ }
+ return nil
+ })
+}
+
+func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, work *progress.WorkDone, uri protocol.DocumentURI, tests, benchmarks []string) error {
+ // TODO: fix the error reporting when this runs async.
+ metas, err := snapshot.MetadataForFile(ctx, uri.SpanURI())
+ if err != nil {
+ return err
+ }
+ metas = source.RemoveIntermediateTestVariants(metas)
+ if len(metas) == 0 {
+ return fmt.Errorf("package could not be found for file: %s", uri.SpanURI().Filename())
+ }
+ pkgPath := string(metas[0].ForTest)
+
+ // create output
+ buf := &bytes.Buffer{}
+ ew := progress.NewEventWriter(ctx, "test")
+ out := io.MultiWriter(ew, progress.NewWorkDoneWriter(ctx, work), buf)
+
+ // Run `go test -run Func` on each test.
+ var failedTests int
+ for _, funcName := range tests {
+ inv := &gocommand.Invocation{
+ Verb: "test",
+ Args: []string{pkgPath, "-v", "-count=1", "-run", fmt.Sprintf("^%s$", funcName)},
+ WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
+ }
+ if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil {
+ if errors.Is(err, context.Canceled) {
+ return err
+ }
+ failedTests++
+ }
+ }
+
+ // Run `go test -run=^$ -bench Func` on each test.
+ var failedBenchmarks int
+ for _, funcName := range benchmarks {
+ inv := &gocommand.Invocation{
+ Verb: "test",
+ Args: []string{pkgPath, "-v", "-run=^$", "-bench", fmt.Sprintf("^%s$", funcName)},
+ WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
+ }
+ if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil {
+ if errors.Is(err, context.Canceled) {
+ return err
+ }
+ failedBenchmarks++
+ }
+ }
+
+ var title string
+ if len(tests) > 0 && len(benchmarks) > 0 {
+ title = "tests and benchmarks"
+ } else if len(tests) > 0 {
+ title = "tests"
+ } else if len(benchmarks) > 0 {
+ title = "benchmarks"
+ } else {
+ return errors.New("No functions were provided")
+ }
+ message := fmt.Sprintf("all %s passed", title)
+ if failedTests > 0 && failedBenchmarks > 0 {
+ message = fmt.Sprintf("%d / %d tests failed and %d / %d benchmarks failed", failedTests, len(tests), failedBenchmarks, len(benchmarks))
+ } else if failedTests > 0 {
+ message = fmt.Sprintf("%d / %d tests failed", failedTests, len(tests))
+ } else if failedBenchmarks > 0 {
+ message = fmt.Sprintf("%d / %d benchmarks failed", failedBenchmarks, len(benchmarks))
+ }
+ if failedTests > 0 || failedBenchmarks > 0 {
+ message += "\n" + buf.String()
+ }
+
+ return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+ Type: protocol.Info,
+ Message: message,
+ })
+}
+
+func (c *commandHandler) Generate(ctx context.Context, args command.GenerateArgs) error {
+ title := "Running go generate ."
+ if args.Recursive {
+ title = "Running go generate ./..."
+ }
+ return c.run(ctx, commandConfig{
+ requireSave: true,
+ progress: title,
+ forURI: args.Dir,
+ }, func(ctx context.Context, deps commandDeps) error {
+ er := progress.NewEventWriter(ctx, "generate")
+
+ pattern := "."
+ if args.Recursive {
+ pattern = "./..."
+ }
+ inv := &gocommand.Invocation{
+ Verb: "generate",
+ Args: []string{"-x", pattern},
+ WorkingDir: args.Dir.SpanURI().Filename(),
+ }
+ stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work))
+ if err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil {
+ return err
+ }
+ return nil
+ })
+}
+
+func (c *commandHandler) GoGetPackage(ctx context.Context, args command.GoGetPackageArgs) error {
+ return c.run(ctx, commandConfig{
+ forURI: args.URI,
+ progress: "Running go get",
+ }, func(ctx context.Context, deps commandDeps) error {
+ // Run on a throwaway go.mod, otherwise it'll write to the real one.
+ stdout, err := deps.snapshot.RunGoCommandDirect(ctx, source.WriteTemporaryModFile|source.AllowNetwork, &gocommand.Invocation{
+ Verb: "list",
+ Args: []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", args.Pkg},
+ WorkingDir: filepath.Dir(args.URI.SpanURI().Filename()),
+ })
+ if err != nil {
+ return err
+ }
+ ver := strings.TrimSpace(stdout.String())
+ return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
+ if args.AddRequire {
+ if err := addModuleRequire(invoke, []string{ver}); err != nil {
+ return err
+ }
+ }
+ _, err := invoke(append([]string{"get", "-d"}, args.Pkg)...)
+ return err
+ })
+ })
+}
+
+func (s *Server) runGoModUpdateCommands(ctx context.Context, snapshot source.Snapshot, uri span.URI, run func(invoke func(...string) (*bytes.Buffer, error)) error) error {
+ tmpModfile, newModBytes, newSumBytes, err := snapshot.RunGoCommands(ctx, true, filepath.Dir(uri.Filename()), run)
+ if err != nil {
+ return err
+ }
+ if !tmpModfile {
+ return nil
+ }
+ modURI := snapshot.GoModForFile(uri)
+ sumURI := span.URIFromPath(strings.TrimSuffix(modURI.Filename(), ".mod") + ".sum")
+ modEdits, err := applyFileEdits(ctx, snapshot, modURI, newModBytes)
+ if err != nil {
+ return err
+ }
+ sumEdits, err := applyFileEdits(ctx, snapshot, sumURI, newSumBytes)
+ if err != nil {
+ return err
+ }
+ changes := append(sumEdits, modEdits...)
+ if len(changes) == 0 {
+ return nil
+ }
+ var documentChanges []protocol.DocumentChanges
+ for _, change := range changes {
+ change := change
+ documentChanges = append(documentChanges, protocol.DocumentChanges{
+ TextDocumentEdit: &change,
+ })
+ }
+ response, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: documentChanges,
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if !response.Applied {
+ return fmt.Errorf("edits not applied because of %s", response.FailureReason)
+ }
+ return nil
+}
+
+func applyFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, newContent []byte) ([]protocol.TextDocumentEdit, error) {
+ fh, err := snapshot.GetFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ oldContent, err := fh.Read()
+ if err != nil && !os.IsNotExist(err) {
+ return nil, err
+ }
+ if bytes.Equal(oldContent, newContent) {
+ return nil, nil
+ }
+
+ // Sending a workspace edit to a closed file causes VS Code to open the
+ // file and leave it unsaved. We would rather apply the changes directly,
+ // especially to go.sum, which should be mostly invisible to the user.
+ if !snapshot.IsOpen(uri) {
+ err := ioutil.WriteFile(uri.Filename(), newContent, 0666)
+ return nil, err
+ }
+
+ m := protocol.NewMapper(fh.URI(), oldContent)
+ diff := snapshot.View().Options().ComputeEdits(string(oldContent), string(newContent))
+ edits, err := source.ToProtocolEdits(m, diff)
+ if err != nil {
+ return nil, err
+ }
+ return []protocol.TextDocumentEdit{{
+ TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
+ Version: fh.Version(),
+ TextDocumentIdentifier: protocol.TextDocumentIdentifier{
+ URI: protocol.URIFromSpanURI(uri),
+ },
+ },
+ Edits: edits,
+ }}, nil
+}
+
+func runGoGetModule(invoke func(...string) (*bytes.Buffer, error), addRequire bool, args []string) error {
+ if addRequire {
+ if err := addModuleRequire(invoke, args); err != nil {
+ return err
+ }
+ }
+ _, err := invoke(append([]string{"get", "-d"}, args...)...)
+ return err
+}
+
+func addModuleRequire(invoke func(...string) (*bytes.Buffer, error), args []string) error {
+ // Using go get to create a new dependency results in an
+ // `// indirect` comment we may not want. The only way to avoid it
+ // is to add the require as direct first. Then we can use go get to
+ // update go.sum and tidy up.
+ _, err := invoke(append([]string{"mod", "edit", "-require"}, args...)...)
+ return err
+}
+
+func (s *Server) getUpgrades(ctx context.Context, snapshot source.Snapshot, uri span.URI, modules []string) (map[string]string, error) {
+ stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal|source.AllowNetwork, &gocommand.Invocation{
+ Verb: "list",
+ Args: append([]string{"-m", "-u", "-json"}, modules...),
+ WorkingDir: filepath.Dir(uri.Filename()),
+ ModFlag: "readonly",
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ upgrades := map[string]string{}
+ for dec := json.NewDecoder(stdout); dec.More(); {
+ mod := &gocommand.ModuleJSON{}
+ if err := dec.Decode(mod); err != nil {
+ return nil, err
+ }
+ if mod.Update == nil {
+ continue
+ }
+ upgrades[mod.Path] = mod.Update.Version
+ }
+ return upgrades, nil
+}
+
+func (c *commandHandler) GCDetails(ctx context.Context, uri protocol.DocumentURI) error {
+ return c.ToggleGCDetails(ctx, command.URIArg{URI: uri})
+}
+
+func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIArg) error {
+ return c.run(ctx, commandConfig{
+ requireSave: true,
+ progress: "Toggling GC Details",
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ metas, err := deps.snapshot.MetadataForFile(ctx, deps.fh.URI())
+ if err != nil {
+ return err
+ }
+ id := metas[0].ID // 0 => narrowest package
+ c.s.gcOptimizationDetailsMu.Lock()
+ if _, ok := c.s.gcOptimizationDetails[id]; ok {
+ delete(c.s.gcOptimizationDetails, id)
+ c.s.clearDiagnosticSource(gcDetailsSource)
+ } else {
+ c.s.gcOptimizationDetails[id] = struct{}{}
+ }
+ c.s.gcOptimizationDetailsMu.Unlock()
+ c.s.diagnoseSnapshot(deps.snapshot, nil, false)
+ return nil
+ })
+}
+
+func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URIArg) (command.ListKnownPackagesResult, error) {
+ var result command.ListKnownPackagesResult
+ err := c.run(ctx, commandConfig{
+ progress: "Listing packages",
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ pkgs, err := source.KnownPackagePaths(ctx, deps.snapshot, deps.fh)
+ for _, pkg := range pkgs {
+ result.Packages = append(result.Packages, string(pkg))
+ }
+ return err
+ })
+ return result, err
+}
+
+func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) (command.ListImportsResult, error) {
+ var result command.ListImportsResult
+ err := c.run(ctx, commandConfig{
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ fh, err := deps.snapshot.GetFile(ctx, args.URI.SpanURI())
+ if err != nil {
+ return err
+ }
+ pgf, err := deps.snapshot.ParseGo(ctx, fh, source.ParseHeader)
+ if err != nil {
+ return err
+ }
+ fset := source.FileSetFor(pgf.Tok)
+ for _, group := range astutil.Imports(fset, pgf.File) {
+ for _, imp := range group {
+ if imp.Path == nil {
+ continue
+ }
+ var name string
+ if imp.Name != nil {
+ name = imp.Name.Name
+ }
+ result.Imports = append(result.Imports, command.FileImport{
+ Path: string(source.UnquoteImportPath(imp)),
+ Name: name,
+ })
+ }
+ }
+ metas, err := deps.snapshot.MetadataForFile(ctx, args.URI.SpanURI())
+ if err != nil {
+ return err // e.g. cancelled
+ }
+ if len(metas) == 0 {
+ return fmt.Errorf("no package containing %v", args.URI.SpanURI())
+ }
+ for pkgPath := range metas[0].DepsByPkgPath { // 0 => narrowest package
+ result.PackageImports = append(result.PackageImports,
+ command.PackageImport{Path: string(pkgPath)})
+ }
+ sort.Slice(result.PackageImports, func(i, j int) bool {
+ return result.PackageImports[i].Path < result.PackageImports[j].Path
+ })
+ return nil
+ })
+ return result, err
+}
+
+func (c *commandHandler) AddImport(ctx context.Context, args command.AddImportArgs) error {
+ return c.run(ctx, commandConfig{
+ progress: "Adding import",
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ edits, err := source.AddImport(ctx, deps.snapshot, deps.fh, args.ImportPath)
+ if err != nil {
+ return fmt.Errorf("could not add import: %v", err)
+ }
+ if _, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: documentChanges(deps.fh, edits),
+ },
+ }); err != nil {
+ return fmt.Errorf("could not apply import edits: %v", err)
+ }
+ return nil
+ })
+}
+
+func (c *commandHandler) StartDebugging(ctx context.Context, args command.DebuggingArgs) (result command.DebuggingResult, _ error) {
+ addr := args.Addr
+ if addr == "" {
+ addr = "localhost:0"
+ }
+ di := debug.GetInstance(ctx)
+ if di == nil {
+ return result, errors.New("internal error: server has no debugging instance")
+ }
+ listenedAddr, err := di.Serve(ctx, addr)
+ if err != nil {
+ return result, fmt.Errorf("starting debug server: %w", err)
+ }
+ result.URLs = []string{"http://" + listenedAddr}
+ return result, nil
+}
+
+// Copy of pkgLoadConfig defined in internal/lsp/cmd/vulncheck.go
+// TODO(hyangah): decide where to define this.
+type pkgLoadConfig struct {
+ // BuildFlags is a list of command-line flags to be passed through to
+ // the build system's query tool.
+ BuildFlags []string
+
+ // If Tests is set, the loader includes related test packages.
+ Tests bool
+}
+
+func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*govulncheck.Result, error) {
+ ret := map[protocol.DocumentURI]*govulncheck.Result{}
+ err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error {
+ if deps.snapshot.View().Options().Vulncheck == source.ModeVulncheckImports {
+ for _, modfile := range deps.snapshot.ModFiles() {
+ res, err := deps.snapshot.ModVuln(ctx, modfile)
+ if err != nil {
+ return err
+ }
+ ret[protocol.URIFromSpanURI(modfile)] = res
+ }
+ }
+ // Overwrite if there is any govulncheck-based result.
+ for modfile, result := range deps.snapshot.View().Vulnerabilities() {
+ ret[protocol.URIFromSpanURI(modfile)] = result
+ }
+ return nil
+ })
+ return ret, err
+}
+
+func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) {
+ if args.URI == "" {
+ return command.RunVulncheckResult{}, errors.New("VulncheckArgs is missing URI field")
+ }
+
+ // Return the workdone token so that clients can identify when this
+ // vulncheck invocation is complete.
+ //
+ // Since the run function executes asynchronously, we use a channel to
+ // synchronize the start of the run and return the token.
+ tokenChan := make(chan protocol.ProgressToken, 1)
+ err := c.run(ctx, commandConfig{
+ async: true, // need to be async to be cancellable
+ progress: "govulncheck",
+ requireSave: true,
+ forURI: args.URI,
+ }, func(ctx context.Context, deps commandDeps) error {
+ tokenChan <- deps.work.Token()
+
+ view := deps.snapshot.View()
+ opts := view.Options()
+ // quickly test if gopls is compiled to support govulncheck
+ // by checking vulncheck.Main. Alternatively, we can continue and
+ // let the `gopls vulncheck` command fail. This is lighter-weight.
+ if vulncheck.Main == nil {
+ return errors.New("vulncheck feature is not available")
+ }
+
+ cmd := exec.CommandContext(ctx, os.Args[0], "vulncheck", "-config", args.Pattern)
+ cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename())
+
+ var viewEnv []string
+ if e := opts.EnvSlice(); e != nil {
+ viewEnv = append(os.Environ(), e...)
+ }
+ cmd.Env = viewEnv
+
+ // stdin: gopls vulncheck expects JSON-encoded configuration from STDIN when -config flag is set.
+ var stdin bytes.Buffer
+ cmd.Stdin = &stdin
+
+ if err := json.NewEncoder(&stdin).Encode(pkgLoadConfig{
+ BuildFlags: opts.BuildFlags,
+ // TODO(hyangah): add `tests` flag in command.VulncheckArgs
+ }); err != nil {
+ return fmt.Errorf("failed to pass package load config: %v", err)
+ }
+
+ // stderr: stream gopls vulncheck's STDERR as progress reports
+ er := progress.NewEventWriter(ctx, "vulncheck")
+ stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work))
+ cmd.Stderr = stderr
+ // TODO: can we stream stdout?
+ stdout, err := cmd.Output()
+ if err != nil {
+ return fmt.Errorf("failed to run govulncheck: %v", err)
+ }
+
+ var result govulncheck.Result
+ if err := json.Unmarshal(stdout, &result); err != nil {
+ // TODO: for easy debugging, log the failed stdout somewhere?
+ return fmt.Errorf("failed to parse govulncheck output: %v", err)
+ }
+ result.Mode = govulncheck.ModeGovulncheck
+ result.AsOf = time.Now()
+ deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), &result)
+
+ c.s.diagnoseSnapshot(deps.snapshot, nil, false)
+ vulns := result.Vulns
+ affecting := make([]string, 0, len(vulns))
+ for _, v := range vulns {
+ if v.IsCalled() {
+ affecting = append(affecting, v.OSV.ID)
+ }
+ }
+ if len(affecting) == 0 {
+ return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+ Type: protocol.Info,
+ Message: "No vulnerabilities found",
+ })
+ }
+ sort.Strings(affecting)
+ return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+ Type: protocol.Warning,
+ Message: fmt.Sprintf("Found %v", strings.Join(affecting, ", ")),
+ })
+ })
+ if err != nil {
+ return command.RunVulncheckResult{}, err
+ }
+ select {
+ case <-ctx.Done():
+ return command.RunVulncheckResult{}, ctx.Err()
+ case token := <-tokenChan:
+ return command.RunVulncheckResult{Token: token}, nil
+ }
+}
+
+// MemStats implements the MemStats command. It returns an error as a
+// future-proof API, but the resulting error is currently always nil.
+func (c *commandHandler) MemStats(ctx context.Context) (command.MemStatsResult, error) {
+ // GC a few times for stable results.
+ runtime.GC()
+ runtime.GC()
+ runtime.GC()
+ var m runtime.MemStats
+ runtime.ReadMemStats(&m)
+ return command.MemStatsResult{
+ HeapAlloc: m.HeapAlloc,
+ HeapInUse: m.HeapInuse,
+ }, nil
+}