diff options
Diffstat (limited to 'gopls/internal/lsp/command.go')
-rw-r--r-- | gopls/internal/lsp/command.go | 964 |
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 +} |