internal/lsp: use gocommand.Invocation more
We pass around verb/args/wd in many places. Bundle them up as an
Invocation instead. goCommandInvocation now updates and returns an input
Invocation.
packages.Config is conceptually an extra layer of parsing and
type-checking on top of a go list invocation. It doesn't make sense for
us to construct the latter using the former. A later CL will construct
the Config in terms of the Invocation; for now duplicate a bit of logic.
Use the environment in the cache key for various module operations
rather than the Config hash. I'm not sure either is fully correct but I
think the environment captures everything that's likely to matter. This
way we don't need Configs in those functions at all.
Change-Id: Iebee2705e63638ab365b3ee18b23f8c3e8ca30ce
Reviewed-on: https://go-review.googlesource.com/c/tools/+/265237
Trust: Heschi Kreinick <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index c7bf6ec..74a3ffb 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -189,6 +189,19 @@
return packageHandleKey(hashContents(b.Bytes()))
}
+// hashEnv returns a hash of the snapshot's configuration.
+func hashEnv(s *snapshot) string {
+ s.view.optionsMu.Lock()
+ env := s.view.options.EnvSlice()
+ s.view.optionsMu.Unlock()
+
+ b := &bytes.Buffer{}
+ for _, e := range env {
+ b.WriteString(e)
+ }
+ return hashContents(b.Bytes())
+}
+
// hashConfig returns the hash for the *packages.Config.
func hashConfig(config *packages.Config) string {
b := bytes.NewBuffer(nil)
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index 0c2a96b..736b56b 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -19,11 +19,11 @@
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/memoize"
- "golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
@@ -165,7 +165,7 @@
// modKey is uniquely identifies cached data for `go mod why` or dependencies
// to upgrade.
type modKey struct {
- sessionID, cfg, view string
+ sessionID, env, view string
mod source.FileIdentity
verb modAction
}
@@ -208,11 +208,9 @@
if handle := s.getModWhyHandle(fh.URI()); handle != nil {
return handle.why(ctx, s)
}
- // Make sure to use the module root as the working directory.
- cfg := s.config(ctx, filepath.Dir(fh.URI().Filename()))
key := modKey{
sessionID: s.view.session.id,
- cfg: hashConfig(cfg),
+ env: hashEnv(s),
mod: fh.FileIdentity(),
view: s.view.rootURI.Filename(),
verb: why,
@@ -232,11 +230,15 @@
return &modWhyData{}
}
// Run `go mod why` on all the dependencies.
- args := []string{"why", "-m"}
- for _, req := range pm.File.Require {
- args = append(args, req.Mod.Path)
+ inv := &gocommand.Invocation{
+ Verb: "mod",
+ Args: []string{"why", "-m"},
+ WorkingDir: filepath.Dir(fh.URI().Filename()),
}
- stdout, err := snapshot.runGoCommandWithConfig(ctx, cfg, "mod", args)
+ for _, req := range pm.File.Require {
+ inv.Args = append(inv.Args, req.Mod.Path)
+ }
+ stdout, err := snapshot.RunGoCommandDirect(ctx, inv)
if err != nil {
return &modWhyData{err: err}
}
@@ -300,11 +302,9 @@
if handle := s.getModUpgradeHandle(fh.URI()); handle != nil {
return handle.upgrades(ctx, s)
}
- // Use the module root as the working directory.
- cfg := s.config(ctx, filepath.Dir(fh.URI().Filename()))
key := modKey{
sessionID: s.view.session.id,
- cfg: hashConfig(cfg),
+ env: hashEnv(s),
mod: fh.FileIdentity(),
view: s.view.rootURI.Filename(),
verb: upgrade,
@@ -326,13 +326,17 @@
}
// Run "go list -mod readonly -u -m all" to be able to see which deps can be
// upgraded without modifying mod file.
- args := []string{"-u", "-m", "-json", "all"}
+ inv := &gocommand.Invocation{
+ Verb: "list",
+ Args: []string{"-u", "-m", "-json", "all"},
+ WorkingDir: filepath.Dir(fh.URI().Filename()),
+ }
if s.workspaceMode()&tempModfile == 0 || containsVendor(fh.URI()) {
// Use -mod=readonly if the module contains a vendor directory
// (see golang/go#38711).
- packagesinternal.SetModFlag(cfg, "readonly")
+ inv.ModFlag = "readonly"
}
- stdout, err := snapshot.runGoCommandWithConfig(ctx, cfg, "list", args)
+ stdout, err := snapshot.RunGoCommandDirect(ctx, inv)
if err != nil {
return &modUpgradeData{err: err}
}
diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go
index 67feb17..375fddc 100644
--- a/internal/lsp/cache/mod_tidy.go
+++ b/internal/lsp/cache/mod_tidy.go
@@ -17,6 +17,7 @@
"golang.org/x/mod/modfile"
"golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol"
@@ -27,7 +28,7 @@
type modTidyKey struct {
sessionID string
- cfg string
+ env string
gomod source.FileIdentity
imports string
unsavedOverlays string
@@ -82,15 +83,13 @@
overlayHash := hashUnsavedOverlays(s.files)
s.mu.Unlock()
- // Make sure to use the module root in the configuration.
- cfg := s.config(ctx, filepath.Dir(fh.URI().Filename()))
key := modTidyKey{
sessionID: s.view.session.id,
view: s.view.folder.Filename(),
imports: importHash,
unsavedOverlays: overlayHash,
gomod: fh.FileIdentity(),
- cfg: hashConfig(cfg),
+ env: hashEnv(s),
}
h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI()))
@@ -114,17 +113,19 @@
err: err,
}
}
- // Get a new config to avoid races, since it may be modified by
- // goCommandInvocation.
- cfg := s.config(ctx, filepath.Dir(fh.URI().Filename()))
- tmpURI, runner, inv, cleanup, err := snapshot.goCommandInvocation(ctx, cfg, true, "mod", []string{"tidy"})
+ inv := &gocommand.Invocation{
+ Verb: "mod",
+ Args: []string{"tidy"},
+ WorkingDir: filepath.Dir(fh.URI().Filename()),
+ }
+ tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, true, inv)
if err != nil {
return &modTidyData{err: err}
}
// Keep the temporary go.mod file around long enough to parse it.
defer cleanup()
- if _, err := runner.Run(ctx, *inv); err != nil {
+ if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
return &modTidyData{err: err}
}
// Go directly to disk to get the temporary mod file, since it is
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 021c534..34d78a1 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -184,7 +184,8 @@
// module.
func (s *snapshot) config(ctx context.Context, dir string) *packages.Config {
s.view.optionsMu.Lock()
- env, buildFlags := s.view.envLocked()
+ env := s.view.options.EnvSlice()
+ buildFlags := append([]string{}, s.view.options.BuildFlags...)
verboseOutput := s.view.options.VerboseOutput
s.view.optionsMu.Unlock()
@@ -192,7 +193,7 @@
Context: ctx,
Dir: dir,
Env: append(append([]string{}, env...), "GO111MODULE="+s.view.go111module),
- BuildFlags: append([]string{}, buildFlags...),
+ BuildFlags: buildFlags,
Mode: packages.NeedName |
packages.NeedFiles |
packages.NeedCompiledGoFiles |
@@ -220,63 +221,48 @@
return cfg
}
-func (s *snapshot) RunGoCommandDirect(ctx context.Context, wd, verb string, args []string) error {
- cfg := s.config(ctx, wd)
- _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, false, verb, args)
- if err != nil {
- return err
- }
- defer cleanup()
-
- _, err = runner.Run(ctx, *inv)
- return err
-}
-
-func (s *snapshot) runGoCommandWithConfig(ctx context.Context, cfg *packages.Config, verb string, args []string) (*bytes.Buffer, error) {
- _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, true, verb, args)
+func (s *snapshot) RunGoCommandDirect(ctx context.Context, inv *gocommand.Invocation) (*bytes.Buffer, error) {
+ _, inv, cleanup, err := s.goCommandInvocation(ctx, false, inv)
if err != nil {
return nil, err
}
defer cleanup()
- return runner.Run(ctx, *inv)
+ return s.view.session.gocmdRunner.Run(ctx, *inv)
}
-func (s *snapshot) RunGoCommandPiped(ctx context.Context, wd, verb string, args []string, stdout, stderr io.Writer) error {
- cfg := s.config(ctx, wd)
- _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, true, verb, args)
+func (s *snapshot) RunGoCommandPiped(ctx context.Context, inv *gocommand.Invocation, stdout, stderr io.Writer) error {
+ _, inv, cleanup, err := s.goCommandInvocation(ctx, true, inv)
if err != nil {
return err
}
defer cleanup()
- return runner.RunPiped(ctx, *inv, stdout, stderr)
+ return s.view.session.gocmdRunner.RunPiped(ctx, *inv, stdout, stderr)
}
-func (s *snapshot) goCommandInvocation(ctx context.Context, cfg *packages.Config, allowTempModfile bool, verb string, args []string) (tmpURI span.URI, runner *gocommand.Runner, inv *gocommand.Invocation, cleanup func(), err error) {
+func (s *snapshot) goCommandInvocation(ctx context.Context, allowTempModfile bool, inv *gocommand.Invocation) (tmpURI span.URI, updatedInv *gocommand.Invocation, cleanup func(), err error) {
+ s.view.optionsMu.Lock()
+ env := s.view.options.EnvSlice()
+ s.view.optionsMu.Unlock()
+
cleanup = func() {} // fallback
- modURI := s.GoModForFile(ctx, span.URIFromPath(cfg.Dir))
+ inv.Env = append(append(append([]string{}, env...), inv.Env...), "GO111MODULE="+s.view.go111module)
- inv = &gocommand.Invocation{
- Verb: verb,
- Args: args,
- Env: cfg.Env,
- WorkingDir: cfg.Dir,
- }
-
+ modURI := s.GoModForFile(ctx, span.URIFromPath(inv.WorkingDir))
if allowTempModfile && s.workspaceMode()&tempModfile != 0 {
if modURI == "" {
- return "", nil, nil, cleanup, fmt.Errorf("no go.mod file found in %s", cfg.Dir)
+ return "", nil, cleanup, fmt.Errorf("no go.mod file found in %s", inv.WorkingDir)
}
modFH, err := s.GetFile(ctx, modURI)
if err != nil {
- return "", nil, nil, cleanup, err
+ return "", nil, cleanup, err
}
// Use the go.sum if it happens to be available.
sumFH, _ := s.sumFH(ctx, modFH)
tmpURI, cleanup, err = tempModFile(modFH, sumFH)
if err != nil {
- return "", nil, nil, cleanup, err
+ return "", nil, cleanup, err
}
inv.ModFile = tmpURI.Filename()
}
@@ -285,23 +271,22 @@
if modURI != "" {
modFH, err := s.GetFile(ctx, modURI)
if err != nil {
- return "", nil, nil, cleanup, err
+ return "", nil, cleanup, err
}
modContent, err = modFH.Read()
if err != nil {
- return "", nil, nil, nil, err
+ return "", nil, nil, err
}
}
modMod, err := s.needsModEqualsMod(ctx, modURI, modContent)
if err != nil {
- return "", nil, nil, cleanup, err
+ return "", nil, cleanup, err
}
if modMod {
inv.ModFlag = "mod"
}
- runner = packagesinternal.GetGoCmdRunner(cfg)
- return tmpURI, runner, inv, cleanup, nil
+ return tmpURI, inv, cleanup, nil
}
func (s *snapshot) buildOverlay() map[string][]byte {
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index b20182a..42017a8 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -285,7 +285,8 @@
func (s *snapshot) WriteEnv(ctx context.Context, w io.Writer) error {
s.view.optionsMu.Lock()
- env, buildFlags := s.view.envLocked()
+ env := s.view.options.EnvSlice()
+ buildFlags := append([]string{}, s.view.options.BuildFlags...)
s.view.optionsMu.Unlock()
fullEnv := make(map[string]string)
@@ -330,14 +331,6 @@
return s.view.importsState.runProcessEnvFunc(ctx, s, fn)
}
-// envLocked returns the environment and build flags for the current view.
-// It assumes that the caller is holding the view's optionsMu.
-func (v *View) envLocked() ([]string, []string) {
- env := append(os.Environ(), v.options.EnvSlice()...)
- buildFlags := append([]string{}, v.options.BuildFlags...)
- return env, buildFlags
-}
-
func (v *View) contains(uri span.URI) bool {
return strings.HasPrefix(string(uri), string(v.rootURI))
}
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 2603a51..80e762c 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -14,6 +14,7 @@
"path/filepath"
"golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
@@ -262,10 +263,14 @@
if err != nil {
return err
}
- wdir := filepath.Dir(uri.SpanURI().Filename())
snapshot, release := view.Snapshot(ctx)
defer release()
- return snapshot.RunGoCommandDirect(ctx, wdir, verb, args)
+ _, err = snapshot.RunGoCommandDirect(ctx, &gocommand.Invocation{
+ Verb: verb,
+ Args: args,
+ WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
+ })
+ return err
}
func (s *Server) runTests(ctx context.Context, snapshot source.Snapshot, uri protocol.DocumentURI, work *workDone, tests, benchmarks []string) error {
@@ -283,13 +288,15 @@
ew := &eventWriter{ctx: ctx, operation: "test"}
out := io.MultiWriter(ew, workDoneWriter{work}, buf)
- wdir := filepath.Dir(uri.SpanURI().Filename())
-
// Run `go test -run Func` on each test.
var failedTests int
for _, funcName := range tests {
- args := []string{pkgPath, "-v", "-count=1", "-run", fmt.Sprintf("^%s$", funcName)}
- if err := snapshot.RunGoCommandPiped(ctx, wdir, "test", args, out, out); err != nil {
+ 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, inv, out, out); err != nil {
if errors.Is(err, context.Canceled) {
return err
}
@@ -300,8 +307,12 @@
// Run `go test -run=^$ -bench Func` on each test.
var failedBenchmarks int
for _, funcName := range benchmarks {
- args := []string{pkgPath, "-v", "-run=^$", "-bench", fmt.Sprintf("^%s$", funcName)}
- if err := snapshot.RunGoCommandPiped(ctx, wdir, "test", args, out, out); err != nil {
+ 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, inv, out, out); err != nil {
if errors.Is(err, context.Canceled) {
return err
}
@@ -344,16 +355,19 @@
defer cancel()
er := &eventWriter{ctx: ctx, operation: "generate"}
- args := []string{"-x"}
+
pattern := "."
if recursive {
pattern = "..."
}
- args = append(args, pattern)
+ inv := &gocommand.Invocation{
+ Verb: "generate",
+ Args: []string{"-x", pattern},
+ WorkingDir: dir.Filename(),
+ }
stderr := io.MultiWriter(er, workDoneWriter{work})
-
- if err := snapshot.RunGoCommandPiped(ctx, dir.Filename(), "generate", args, er, stderr); err != nil {
+ if err := snapshot.RunGoCommandPiped(ctx, inv, er, stderr); err != nil {
return err
}
return nil
diff --git a/internal/lsp/source/gc_annotations.go b/internal/lsp/source/gc_annotations.go
index 6494b6a..0a0d2b5 100644
--- a/internal/lsp/source/gc_annotations.go
+++ b/internal/lsp/source/gc_annotations.go
@@ -14,6 +14,7 @@
"path/filepath"
"strings"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
)
@@ -36,12 +37,16 @@
if !strings.HasPrefix(outDir, "/") {
outDirURI = span.URI(strings.Replace(string(outDirURI), "file:///", "file://", 1))
}
- args := []string{
- fmt.Sprintf("-gcflags=-json=0,%s", outDirURI),
- fmt.Sprintf("-o=%s", tmpFile.Name()),
- ".",
+ inv := &gocommand.Invocation{
+ Verb: "build",
+ Args: []string{
+ fmt.Sprintf("-gcflags=-json=0,%s", outDirURI),
+ fmt.Sprintf("-o=%s", tmpFile.Name()),
+ ".",
+ },
+ WorkingDir: pkgDir.Filename(),
}
- err = snapshot.RunGoCommandDirect(ctx, pkgDir.Filename(), "build", args)
+ _, err = snapshot.RunGoCommandDirect(ctx, inv)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index c84ff06..2f3a212 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -5,6 +5,7 @@
package source
import (
+ "bytes"
"context"
"fmt"
"go/ast"
@@ -16,6 +17,7 @@
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
@@ -80,16 +82,13 @@
// Analyze runs the analyses for the given package at this snapshot.
Analyze(ctx context.Context, pkgID string, analyzers ...*analysis.Analyzer) ([]*Error, error)
- // RunGoCommandPiped runs the given `go` command in the view, using the
- // provided stdout and stderr. It will use the -modfile flag, if possible.
- // If the provided working directory is empty, the snapshot's root folder
- // will be used as the working directory.
- RunGoCommandPiped(ctx context.Context, wd, verb string, args []string, stdout, stderr io.Writer) error
+ // RunGoCommandPiped runs the given `go` command, writing its output
+ // to stdout and stderr. Verb, Args, and WorkingDir must be specified.
+ RunGoCommandPiped(ctx context.Context, inv *gocommand.Invocation, stdout, stderr io.Writer) error
- // RunGoCommandDirect runs the given `go` command, never using the
- // -modfile flag. If the provided working directory is empty, the
- // snapshot's root folder will be used as the working directory.
- RunGoCommandDirect(ctx context.Context, wd, verb string, args []string) error
+ // RunGoCommandDirect runs the given `go` command. Verb, Args, and
+ // WorkingDir must be specified.
+ RunGoCommandDirect(ctx context.Context, inv *gocommand.Invocation) (*bytes.Buffer, error)
// RunProcessEnvFunc runs fn with the process env for this snapshot's view.
// Note: the process env contains cached module and filesystem state.