Skip to content

Commit 29f4edb

Browse files
committed
gopls/internal/cache: simplify usage of snapshot.GoCommandInvocation
The Snapshot.GoCommandInvocation API is a source of confusion, because it passes in a gocommand.Invocation, performs arbitrary mutation, and then passes it back to the caller (which may perform mutation on top). Aside from the readability problems of this indirectness, this API can lead to real bugs, for example if one of the mutations is incompatible with another. Furthermore, the fact that the GoCommandInvocation is the golden source of information about the Go command environment leads to awkwardness and redundant work. For example, to get the packages.Config we called Snapshot.GoCommandInvocation, just to read env, working dir, and build flags (and the now irrelevant ModFile and ModFlag). But GoCommandInvocation wrote overlays, so we'd end up writing overlays twice: once within the gopls layer, and then again in the go/packages go list driver. Simplify as follows: - Pass in dir, verb, args, and env to GoCommandInvocation, to avoid passing around and mutating an Invocation. - Extract the View.Env, which is a useful concept, and apply invocation env on top. Surveying existing use cases that this was correct, as all call sites expected their env not to be overwritten. - Move Snapshot.config to load.go, where it belongs, and simplify not to depend on GoCommandInvocation. Also add an additional test case for BenchmarkReload. Change-Id: I8ae7a13a033360e0e7b0b24ff718b5a22123e99c Reviewed-on: https://go-review.googlesource.com/c/tools/+/626715 Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Robert Findley <rfindley@google.com>
1 parent 3c20e3f commit 29f4edb

File tree

9 files changed

+167
-178
lines changed

9 files changed

+167
-178
lines changed

gopls/internal/cache/load.go

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"errors"
1111
"fmt"
12+
"go/types"
1213
"path/filepath"
1314
"slices"
1415
"sort"
@@ -25,8 +26,8 @@ import (
2526
"golang.org/x/tools/gopls/internal/util/immutable"
2627
"golang.org/x/tools/gopls/internal/util/pathutil"
2728
"golang.org/x/tools/internal/event"
28-
"golang.org/x/tools/internal/gocommand"
2929
"golang.org/x/tools/internal/packagesinternal"
30+
"golang.org/x/tools/internal/typesinternal"
3031
"golang.org/x/tools/internal/xcontext"
3132
)
3233

@@ -57,6 +58,7 @@ func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
5758
// Keep track of module query -> module path so that we can later correlate query
5859
// errors with errors.
5960
moduleQueries := make(map[string]string)
61+
6062
for _, scope := range scopes {
6163
switch scope := scope.(type) {
6264
case packageLoadScope:
@@ -118,21 +120,13 @@ func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
118120

119121
startTime := time.Now()
120122

121-
inv, cleanupInvocation, err := s.GoCommandInvocation(allowNetwork, &gocommand.Invocation{
122-
WorkingDir: s.view.root.Path(),
123-
})
124-
if err != nil {
125-
return err
126-
}
127-
defer cleanupInvocation()
128-
129123
// Set a last resort deadline on packages.Load since it calls the go
130124
// command, which may hang indefinitely if it has a bug. golang/go#42132
131125
// and golang/go#42255 have more context.
132126
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
133127
defer cancel()
134128

135-
cfg := s.config(ctx, inv)
129+
cfg := s.config(ctx, allowNetwork)
136130
pkgs, err := packages.Load(cfg, query...)
137131

138132
// If the context was canceled, return early. Otherwise, we might be
@@ -278,7 +272,7 @@ func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
278272
if allFilesExcluded(pkg.GoFiles, filterFunc) {
279273
continue
280274
}
281-
buildMetadata(newMetadata, pkg, cfg.Dir, standalone, s.view.typ != GoPackagesDriverView)
275+
buildMetadata(newMetadata, cfg.Dir, standalone, pkg)
282276
}
283277

284278
s.mu.Lock()
@@ -362,14 +356,56 @@ func (m *moduleErrorMap) Error() string {
362356
return buf.String()
363357
}
364358

359+
// config returns the configuration used for the snapshot's interaction with
360+
// the go/packages API. It uses the given working directory.
361+
//
362+
// TODO(rstambler): go/packages requires that we do not provide overlays for
363+
// multiple modules in one config, so buildOverlay needs to filter overlays by
364+
// module.
365+
// TODO(rfindley): ^^ is this still true?
366+
func (s *Snapshot) config(ctx context.Context, allowNetwork bool) *packages.Config {
367+
cfg := &packages.Config{
368+
Context: ctx,
369+
Dir: s.view.root.Path(),
370+
Env: s.view.Env(),
371+
BuildFlags: slices.Clone(s.view.folder.Options.BuildFlags),
372+
Mode: packages.NeedName |
373+
packages.NeedFiles |
374+
packages.NeedCompiledGoFiles |
375+
packages.NeedImports |
376+
packages.NeedDeps |
377+
packages.NeedTypesSizes |
378+
packages.NeedModule |
379+
packages.NeedEmbedFiles |
380+
packages.LoadMode(packagesinternal.DepsErrors) |
381+
packages.NeedForTest,
382+
Fset: nil, // we do our own parsing
383+
Overlay: s.buildOverlays(),
384+
Logf: func(format string, args ...interface{}) {
385+
if s.view.folder.Options.VerboseOutput {
386+
event.Log(ctx, fmt.Sprintf(format, args...))
387+
}
388+
},
389+
Tests: true,
390+
}
391+
if !allowNetwork {
392+
cfg.Env = append(cfg.Env, "GOPROXY=off")
393+
}
394+
// We want to type check cgo code if go/types supports it.
395+
if typesinternal.SetUsesCgo(&types.Config{}) {
396+
cfg.Mode |= packages.LoadMode(packagesinternal.TypecheckCgo)
397+
}
398+
return cfg
399+
}
400+
365401
// buildMetadata populates the updates map with metadata updates to
366402
// apply, based on the given pkg. It recurs through pkg.Imports to ensure that
367403
// metadata exists for all dependencies.
368404
//
369405
// Returns the metadata.Package that was built (or which was already present in
370406
// updates), or nil if the package could not be built. Notably, the resulting
371407
// metadata.Package may have an ID that differs from pkg.ID.
372-
func buildMetadata(updates map[PackageID]*metadata.Package, pkg *packages.Package, loadDir string, standalone, goListView bool) *metadata.Package {
408+
func buildMetadata(updates map[PackageID]*metadata.Package, loadDir string, standalone bool, pkg *packages.Package) *metadata.Package {
373409
// Allow for multiple ad-hoc packages in the workspace (see #47584).
374410
pkgPath := PackagePath(pkg.PkgPath)
375411
id := PackageID(pkg.ID)
@@ -524,7 +560,7 @@ func buildMetadata(updates map[PackageID]*metadata.Package, pkg *packages.Packag
524560
continue
525561
}
526562

527-
dep := buildMetadata(updates, imported, loadDir, false, goListView) // only top level packages can be standalone
563+
dep := buildMetadata(updates, loadDir, false, imported) // only top level packages can be standalone
528564

529565
// Don't record edges to packages with no name, as they cause trouble for
530566
// the importer (golang/go#60952).

gopls/internal/cache/mod.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"golang.org/x/tools/gopls/internal/protocol"
2020
"golang.org/x/tools/gopls/internal/protocol/command"
2121
"golang.org/x/tools/internal/event"
22-
"golang.org/x/tools/internal/gocommand"
2322
"golang.org/x/tools/internal/memoize"
2423
)
2524

@@ -252,11 +251,7 @@ func modWhyImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) (map[st
252251
for _, req := range pm.File.Require {
253252
args = append(args, req.Mod.Path)
254253
}
255-
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{
256-
Verb: "mod",
257-
Args: args,
258-
WorkingDir: filepath.Dir(fh.URI().Path()),
259-
})
254+
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, filepath.Dir(fh.URI().Path()), "mod", args)
260255
if err != nil {
261256
return nil, err
262257
}

gopls/internal/cache/mod_tidy.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"golang.org/x/tools/gopls/internal/protocol/command"
2424
"golang.org/x/tools/internal/diff"
2525
"golang.org/x/tools/internal/event"
26-
"golang.org/x/tools/internal/gocommand"
2726
"golang.org/x/tools/internal/memoize"
2827
)
2928

@@ -108,12 +107,13 @@ func modTidyImpl(ctx context.Context, snapshot *Snapshot, pm *ParsedModule) (*Ti
108107
}
109108
defer cleanup()
110109

111-
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{
112-
Verb: "mod",
113-
Args: []string{"tidy", "-modfile=" + filepath.Join(tempDir, "go.mod")},
114-
Env: []string{"GOWORK=off"},
115-
WorkingDir: pm.URI.Dir().Path(),
116-
})
110+
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(
111+
false,
112+
pm.URI.Dir().Path(),
113+
"mod",
114+
[]string{"tidy", "-modfile=" + filepath.Join(tempDir, "go.mod")},
115+
"GOWORK=off",
116+
)
117117
if err != nil {
118118
return nil, err
119119
}

gopls/internal/cache/snapshot.go

Lines changed: 14 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"go/build/constraint"
1414
"go/parser"
1515
"go/token"
16-
"go/types"
1716
"os"
1817
"path"
1918
"path/filepath"
@@ -26,7 +25,6 @@ import (
2625
"sync"
2726

2827
"golang.org/x/sync/errgroup"
29-
"golang.org/x/tools/go/packages"
3028
"golang.org/x/tools/go/types/objectpath"
3129
"golang.org/x/tools/gopls/internal/cache/metadata"
3230
"golang.org/x/tools/gopls/internal/cache/methodsets"
@@ -49,8 +47,6 @@ import (
4947
"golang.org/x/tools/internal/event/label"
5048
"golang.org/x/tools/internal/gocommand"
5149
"golang.org/x/tools/internal/memoize"
52-
"golang.org/x/tools/internal/packagesinternal"
53-
"golang.org/x/tools/internal/typesinternal"
5450
)
5551

5652
// A Snapshot represents the current state for a given view.
@@ -363,50 +359,6 @@ func (s *Snapshot) Templates() map[protocol.DocumentURI]file.Handle {
363359
return tmpls
364360
}
365361

366-
// config returns the configuration used for the snapshot's interaction with
367-
// the go/packages API. It uses the given working directory.
368-
//
369-
// TODO(rstambler): go/packages requires that we do not provide overlays for
370-
// multiple modules in one config, so buildOverlay needs to filter overlays by
371-
// module.
372-
func (s *Snapshot) config(ctx context.Context, inv *gocommand.Invocation) *packages.Config {
373-
374-
cfg := &packages.Config{
375-
Context: ctx,
376-
Dir: inv.WorkingDir,
377-
Env: inv.Env,
378-
BuildFlags: inv.BuildFlags,
379-
Mode: packages.NeedName |
380-
packages.NeedFiles |
381-
packages.NeedCompiledGoFiles |
382-
packages.NeedImports |
383-
packages.NeedDeps |
384-
packages.NeedTypesSizes |
385-
packages.NeedModule |
386-
packages.NeedEmbedFiles |
387-
packages.LoadMode(packagesinternal.DepsErrors) |
388-
packages.NeedForTest,
389-
Fset: nil, // we do our own parsing
390-
Overlay: s.buildOverlays(),
391-
ParseFile: func(*token.FileSet, string, []byte) (*ast.File, error) {
392-
panic("go/packages must not be used to parse files")
393-
},
394-
Logf: func(format string, args ...interface{}) {
395-
if s.Options().VerboseOutput {
396-
event.Log(ctx, fmt.Sprintf(format, args...))
397-
}
398-
},
399-
Tests: true,
400-
}
401-
packagesinternal.SetModFile(cfg, inv.ModFile)
402-
packagesinternal.SetModFlag(cfg, inv.ModFlag)
403-
// We want to type check cgo code if go/types supports it.
404-
if typesinternal.SetUsesCgo(&types.Config{}) {
405-
cfg.Mode |= packages.LoadMode(packagesinternal.TypecheckCgo)
406-
}
407-
return cfg
408-
}
409-
410362
// RunGoModUpdateCommands runs a series of `go` commands that updates the go.mod
411363
// and go.sum file for wd, and returns their updated contents.
412364
//
@@ -423,16 +375,14 @@ func (s *Snapshot) RunGoModUpdateCommands(ctx context.Context, modURI protocol.D
423375
// TODO(rfindley): we must use ModFlag and ModFile here (rather than simply
424376
// setting Args), because without knowing the verb, we can't know whether
425377
// ModFlag is appropriate. Refactor so that args can be set by the caller.
426-
inv, cleanupInvocation, err := s.GoCommandInvocation(true, &gocommand.Invocation{
427-
WorkingDir: modURI.Dir().Path(),
428-
ModFlag: "mod",
429-
ModFile: filepath.Join(tempDir, "go.mod"),
430-
Env: []string{"GOWORK=off"},
431-
})
378+
inv, cleanupInvocation, err := s.GoCommandInvocation(true, modURI.Dir().Path(), "", nil, "GOWORK=off")
432379
if err != nil {
433380
return nil, nil, err
434381
}
435382
defer cleanupInvocation()
383+
384+
inv.ModFlag = "mod"
385+
inv.ModFile = filepath.Join(tempDir, "go.mod")
436386
invoke := func(args ...string) (*bytes.Buffer, error) {
437387
inv.Verb = args[0]
438388
inv.Args = args[1:]
@@ -510,22 +460,16 @@ func TempModDir(ctx context.Context, fs file.Source, modURI protocol.DocumentURI
510460
// BuildFlags should be more clearly expressed in the API.
511461
//
512462
// If allowNetwork is set, do not set GOPROXY=off.
513-
func (s *Snapshot) GoCommandInvocation(allowNetwork bool, inv *gocommand.Invocation) (_ *gocommand.Invocation, cleanup func(), _ error) {
514-
// TODO(rfindley): it's not clear that this is doing the right thing.
515-
// Should inv.Env really overwrite view.options? Should s.view.envOverlay
516-
// overwrite inv.Env? (Do we ever invoke this with a non-empty inv.Env?)
517-
//
518-
// We should survey existing uses and write down rules for how env is
519-
// applied.
520-
inv.Env = slices.Concat(
521-
os.Environ(),
522-
s.Options().EnvSlice(),
523-
inv.Env,
524-
[]string{"GO111MODULE=" + s.view.adjustedGO111MODULE()},
525-
s.view.EnvOverlay(),
526-
)
527-
inv.BuildFlags = slices.Clone(s.Options().BuildFlags)
528-
463+
//
464+
// TODO(rfindley): use a named boolean for allowNetwork.
465+
func (s *Snapshot) GoCommandInvocation(allowNetwork bool, dir, verb string, args []string, env ...string) (_ *gocommand.Invocation, cleanup func(), _ error) {
466+
inv := &gocommand.Invocation{
467+
Verb: verb,
468+
Args: args,
469+
WorkingDir: dir,
470+
Env: append(s.view.Env(), env...),
471+
BuildFlags: slices.Clone(s.Options().BuildFlags),
472+
}
529473
if !allowNetwork {
530474
inv.Env = append(inv.Env, "GOPROXY=off")
531475
}

gopls/internal/cache/view.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,16 @@ func (v *View) Folder() *Folder {
356356
return v.folder
357357
}
358358

359+
// Env returns the environment to use for running go commands in this view.
360+
func (v *View) Env() []string {
361+
return slices.Concat(
362+
os.Environ(),
363+
v.folder.Options.EnvSlice(),
364+
[]string{"GO111MODULE=" + v.adjustedGO111MODULE()},
365+
v.EnvOverlay(),
366+
)
367+
}
368+
359369
// UpdateFolders updates the set of views for the new folders.
360370
//
361371
// Calling this causes each view to be reinitialized.

gopls/internal/golang/assembly.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@ import (
1616
"context"
1717
"fmt"
1818
"html"
19-
"path/filepath"
2019
"regexp"
2120
"strconv"
2221
"strings"
2322

2423
"golang.org/x/tools/gopls/internal/cache"
25-
"golang.org/x/tools/internal/gocommand"
2624
)
2725

2826
// AssemblyHTML returns an HTML document containing an assembly listing of the selected function.
@@ -32,11 +30,7 @@ import (
3230
// - cross-link jumps and block labels, like github.com/aclements/objbrowse.
3331
func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string, web Web) ([]byte, error) {
3432
// Compile the package with -S, and capture its stderr stream.
35-
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{
36-
Verb: "build",
37-
Args: []string{"-gcflags=-S", "."},
38-
WorkingDir: filepath.Dir(pkg.Metadata().CompiledGoFiles[0].Path()),
39-
})
33+
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, pkg.Metadata().CompiledGoFiles[0].Dir().Path(), "build", []string{"-gcflags=-S", "."})
4034
if err != nil {
4135
return nil, err // e.g. failed to write overlays (rare)
4236
}

gopls/internal/golang/gc_annotations.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"golang.org/x/tools/gopls/internal/protocol"
1919
"golang.org/x/tools/gopls/internal/settings"
2020
"golang.org/x/tools/internal/event"
21-
"golang.org/x/tools/internal/gocommand"
2221
)
2322

2423
// GCOptimizationDetails invokes the Go compiler on the specified
@@ -57,14 +56,10 @@ func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *me
5756
if !strings.HasPrefix(outDir, "/") {
5857
outDirURI = protocol.DocumentURI(strings.Replace(string(outDirURI), "file:///", "file://", 1))
5958
}
60-
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{
61-
Verb: "build",
62-
Args: []string{
63-
fmt.Sprintf("-gcflags=-json=0,%s", outDirURI),
64-
fmt.Sprintf("-o=%s", tmpFile.Name()),
65-
".",
66-
},
67-
WorkingDir: pkgDir,
59+
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, pkgDir, "build", []string{
60+
fmt.Sprintf("-gcflags=-json=0,%s", outDirURI),
61+
fmt.Sprintf("-o=%s", tmpFile.Name()),
62+
".",
6863
})
6964
if err != nil {
7065
return nil, err

0 commit comments

Comments
 (0)