1
0
mirror of https://github.com/golang/go synced 2024-11-18 13:04:46 -07:00

internal/lsp/cache: add concurrency error check for go cmds

This change attempts to fix a concurrency error that would cause
textDocument/CodeLens, textDocument/Formatting, textDocument/DocumentLink,
and textDocument/Hover from failing on go.mod files.

The issue was that the go command would return a potential concurrency
error since the ModHandle and the ModTidyHandle are both using the
temporary go.mod file.

Updates golang/go#37824

Change-Id: I6cd63c1f75817c7308e033aec473966536a2a3bd
Reviewed-on: https://go-review.googlesource.com/c/tools/+/224917
Reviewed-by: Heschi Kreinick <heschi@google.com>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
Rohan Challa 2020-03-23 08:35:36 -04:00 committed by Rebecca Stambler
parent 4d14fc9c00
commit 46bd65c853
20 changed files with 157 additions and 97 deletions

View File

@ -19,8 +19,7 @@ import (
var debug = false
// GetSizes returns the sizes used by the underlying driver with the given parameters.
func GetSizes(ctx context.Context, buildFlags, env []string, dir string, usesExportData bool) (types.Sizes, error) {
func GetSizes(ctx context.Context, buildFlags, env []string, gocmdRunner *gocommand.Runner, dir string) (types.Sizes, error) {
// TODO(matloob): Clean this up. This code is mostly a copy of packages.findExternalDriver.
const toolPrefix = "GOPACKAGESDRIVER="
tool := ""
@ -40,7 +39,7 @@ func GetSizes(ctx context.Context, buildFlags, env []string, dir string, usesExp
}
if tool == "off" {
return GetSizesGolist(ctx, buildFlags, env, dir, usesExportData)
return GetSizesGolist(ctx, buildFlags, env, gocmdRunner, dir)
}
req, err := json.Marshal(struct {
@ -76,7 +75,7 @@ func GetSizes(ctx context.Context, buildFlags, env []string, dir string, usesExp
return response.Sizes, nil
}
func GetSizesGolist(ctx context.Context, buildFlags, env []string, dir string, usesExportData bool) (types.Sizes, error) {
func GetSizesGolist(ctx context.Context, buildFlags, env []string, gocmdRunner *gocommand.Runner, dir string) (types.Sizes, error) {
inv := gocommand.Invocation{
Verb: "list",
Args: []string{"-f", "{{context.GOARCH}} {{context.Compiler}}", "--", "unsafe"},
@ -84,7 +83,7 @@ func GetSizesGolist(ctx context.Context, buildFlags, env []string, dir string, u
BuildFlags: buildFlags,
WorkingDir: dir,
}
stdout, stderr, friendlyErr, rawErr := inv.RunRaw(ctx)
stdout, stderr, friendlyErr, rawErr := gocmdRunner.RunRaw(ctx, inv)
var goarch, compiler string
if rawErr != nil {
if strings.Contains(rawErr.Error(), "cannot find main module") {
@ -96,7 +95,7 @@ func GetSizesGolist(ctx context.Context, buildFlags, env []string, dir string, u
Env: env,
WorkingDir: dir,
}
envout, enverr := inv.Run(ctx)
envout, enverr := gocmdRunner.Run(ctx, inv)
if enverr != nil {
return nil, enverr
}

View File

@ -143,7 +143,7 @@ func goListDriver(cfg *Config, patterns ...string) (*driverResponse, error) {
sizeswg.Add(1)
go func() {
var sizes types.Sizes
sizes, sizeserr = packagesdriver.GetSizesGolist(ctx, cfg.BuildFlags, cfg.Env, cfg.Dir, usesExportData(cfg))
sizes, sizeserr = packagesdriver.GetSizesGolist(ctx, cfg.BuildFlags, cfg.Env, cfg.gocmdRunner, cfg.Dir)
// types.SizesFor always returns nil or a *types.StdSizes.
response.dr.Sizes, _ = sizes.(*types.StdSizes)
sizeswg.Done()
@ -717,7 +717,7 @@ func golistargs(cfg *Config, words []string) []string {
func (state *golistState) invokeGo(verb string, args ...string) (*bytes.Buffer, error) {
cfg := state.cfg
inv := &gocommand.Invocation{
inv := gocommand.Invocation{
Verb: verb,
Args: args,
BuildFlags: cfg.BuildFlags,
@ -725,8 +725,11 @@ func (state *golistState) invokeGo(verb string, args ...string) (*bytes.Buffer,
Logf: cfg.Logf,
WorkingDir: cfg.Dir,
}
stdout, stderr, _, err := inv.RunRaw(cfg.Context)
gocmdRunner := cfg.gocmdRunner
if gocmdRunner == nil {
gocmdRunner = &gocommand.Runner{}
}
stdout, stderr, _, err := gocmdRunner.RunRaw(cfg.Context, inv)
if err != nil {
// Check for 'go' executable not being found.
if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound {

View File

@ -23,6 +23,7 @@ import (
"sync"
"golang.org/x/tools/go/gcexportdata"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/packagesinternal"
)
@ -127,6 +128,9 @@ type Config struct {
//
Env []string
// gocmdRunner guards go command calls from concurrency errors.
gocmdRunner *gocommand.Runner
// BuildFlags is a list of command-line flags to be passed through to
// the build system's query tool.
BuildFlags []string
@ -311,6 +315,12 @@ func init() {
packagesinternal.GetModule = func(p interface{}) *packagesinternal.Module {
return p.(*Package).module
}
packagesinternal.GetGoCmdRunner = func(config interface{}) *gocommand.Runner {
return config.(*Config).gocmdRunner
}
packagesinternal.SetGoCmdRunner = func(config interface{}, runner *gocommand.Runner) {
config.(*Config).gocmdRunner = runner
}
}
// An Error describes a problem with a package's metadata, syntax, or types.
@ -473,6 +483,9 @@ func newLoader(cfg *Config) *loader {
if ld.Config.Env == nil {
ld.Config.Env = os.Environ()
}
if ld.Config.gocmdRunner == nil {
ld.Config.gocmdRunner = &gocommand.Runner{}
}
if ld.Context == nil {
ld.Context = context.Background()
}

View File

@ -16,6 +16,7 @@ import (
"strings"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/packagesinternal"
)
// Modules is the exporter that produces module layouts.
@ -166,6 +167,8 @@ func (modules) Finalize(exported *Exported) error {
"GOPROXY="+proxyDirToURL(proxyDir),
"GOSUMDB=off",
)
gocmdRunner := &gocommand.Runner{}
packagesinternal.SetGoCmdRunner(exported.Config, gocmdRunner)
// Run go mod download to recreate the mod cache dir with all the extra
// stuff in cache. All the files created by Export should be recreated.
@ -176,7 +179,7 @@ func (modules) Finalize(exported *Exported) error {
BuildFlags: exported.Config.BuildFlags,
WorkingDir: exported.Config.Dir,
}
if _, err := inv.Run(context.Background()); err != nil {
if _, err := gocmdRunner.Run(context.Background(), inv); err != nil {
return err
}
return nil

View File

@ -12,10 +12,70 @@ import (
"io"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"time"
"golang.org/x/tools/internal/telemetry/event"
)
// An Runner will run go command invocations and serialize
// them if it sees a concurrency error.
type Runner struct {
// LoadMu guards packages.Load calls and associated state.
loadMu sync.Mutex
serializeLoads int
}
// 1.13: go: updates to go.mod needed, but contents have changed
// 1.14: go: updating go.mod: existing contents have changed since last read
var modConcurrencyError = regexp.MustCompile(`go:.*go.mod.*contents have changed`)
// Run calls Runner.RunRaw, serializing requests if they fight over
// go.mod changes.
func (runner *Runner) Run(ctx context.Context, inv Invocation) (*bytes.Buffer, error) {
stdout, _, friendly, _ := runner.RunRaw(ctx, inv)
return stdout, friendly
}
// Run calls Innvocation.RunRaw, serializing requests if they fight over
// go.mod changes.
func (runner *Runner) RunRaw(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) {
// We want to run invocations concurrently as much as possible. However,
// if go.mod updates are needed, only one can make them and the others will
// fail. We need to retry in those cases, but we don't want to thrash so
// badly we never recover. To avoid that, once we've seen one concurrency
// error, start serializing everything until the backlog has cleared out.
runner.loadMu.Lock()
var locked bool // If true, we hold the mutex and have incremented.
if runner.serializeLoads == 0 {
runner.loadMu.Unlock()
} else {
locked = true
runner.serializeLoads++
}
defer func() {
if locked {
runner.serializeLoads--
runner.loadMu.Unlock()
}
}()
for {
stdout, stderr, friendlyErr, err := inv.runRaw(ctx)
if friendlyErr == nil || !modConcurrencyError.MatchString(friendlyErr.Error()) {
return stdout, stderr, friendlyErr, err
}
event.Error(ctx, "Load concurrency error, will retry serially", err)
if !locked {
runner.loadMu.Lock()
runner.serializeLoads++
locked = true
}
}
}
// An Invocation represents a call to the go command.
type Invocation struct {
Verb string
@ -26,16 +86,9 @@ type Invocation struct {
Logf func(format string, args ...interface{})
}
// Run runs the invocation, returning its stdout and an error suitable for
// human consumption, including stderr.
func (i *Invocation) Run(ctx context.Context) (*bytes.Buffer, error) {
stdout, _, friendly, _ := i.RunRaw(ctx)
return stdout, friendly
}
// RunRaw is like RunPiped, but also returns the raw stderr and error for callers
// that want to do low-level error handling/recovery.
func (i *Invocation) RunRaw(ctx context.Context) (stdout *bytes.Buffer, stderr *bytes.Buffer, friendlyError error, rawError error) {
func (i *Invocation) runRaw(ctx context.Context) (stdout *bytes.Buffer, stderr *bytes.Buffer, friendlyError error, rawError error) {
stdout = &bytes.Buffer{}
stderr = &bytes.Buffer{}
rawError = i.RunPiped(ctx, stdout, stderr)

View File

@ -15,7 +15,8 @@ func TestGoVersion(t *testing.T) {
inv := gocommand.Invocation{
Verb: "version",
}
if _, err := inv.Run(context.Background()); err != nil {
gocmdRunner := &gocommand.Runner{}
if _, err := gocmdRunner.Run(context.Background(), inv); err != nil {
t.Error(err)
}
}

View File

@ -747,6 +747,8 @@ func getPackageExports(ctx context.Context, wrapped func(PackageExport), searchP
type ProcessEnv struct {
LocalPrefix string
GocmdRunner *gocommand.Runner
BuildFlags []string
// If non-empty, these will be used instead of the
@ -830,7 +832,7 @@ func (e *ProcessEnv) invokeGo(ctx context.Context, verb string, args ...string)
Logf: e.Logf,
WorkingDir: e.WorkingDir,
}
return inv.Run(ctx)
return e.GocmdRunner.Run(ctx, inv)
}
func addStdlibCandidates(pass *pass, refs references) {

View File

@ -19,6 +19,7 @@ import (
"testing"
"golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/gocommand"
)
var testDebug = flag.Bool("debug", false, "enable debug output")
@ -1638,6 +1639,7 @@ func (c testConfig) test(t *testing.T, fn func(*goimportTest)) {
GO111MODULE: env["GO111MODULE"],
GOSUMDB: env["GOSUMDB"],
WorkingDir: exported.Config.Dir,
GocmdRunner: &gocommand.Runner{},
},
exported: exported,
}

View File

@ -27,6 +27,7 @@ import (
"strings"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/gocommand"
)
// Options is golang.org/x/tools/imports.Options with extra internal-only options.
@ -154,6 +155,10 @@ func initialize(filename string, src []byte, opt *Options) ([]byte, *Options, er
GOSUMDB: os.Getenv("GOSUMDB"),
}
}
// Set the gocmdRunner if the user has not provided it.
if opt.Env.GocmdRunner == nil {
opt.Env.GocmdRunner = &gocommand.Runner{}
}
if src == nil {
b, err := ioutil.ReadFile(filename)
if err != nil {

View File

@ -5,6 +5,7 @@ import (
"os"
"testing"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/testenv"
)
@ -33,6 +34,7 @@ func TestNilOpts(t *testing.T) {
Env: &ProcessEnv{
GOPATH: build.Default.GOPATH,
GOROOT: build.Default.GOROOT,
GocmdRunner: &gocommand.Runner{},
},
Comments: true,
TabIndent: true,

View File

@ -19,6 +19,7 @@ import (
"testing"
"golang.org/x/mod/module"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/gopathwalk"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/txtar"
@ -676,6 +677,7 @@ func setup(t *testing.T, main, wd string) *modTest {
GOPROXY: proxyDirToURL(proxyDir),
GOSUMDB: "off",
WorkingDir: filepath.Join(mainDir, wd),
GocmdRunner: &gocommand.Runner{},
}
if *testDebug {
env.Logf = log.Printf
@ -850,6 +852,7 @@ func TestInvalidModCache(t *testing.T) {
GOPATH: filepath.Join(dir, "gopath"),
GO111MODULE: "on",
GOSUMDB: "off",
GocmdRunner: &gocommand.Runner{},
WorkingDir: dir,
}
resolver := newModuleResolver(env)
@ -920,6 +923,7 @@ func BenchmarkScanModCache(b *testing.B) {
env := &ProcessEnv{
GOPATH: build.Default.GOPATH,
GOROOT: build.Default.GOROOT,
GocmdRunner: &gocommand.Runner{},
Logf: log.Printf,
}
exclude := []gopathwalk.RootType{gopathwalk.RootGOROOT}

View File

@ -84,7 +84,7 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error {
defer done()
cfg := s.Config(ctx)
pkgs, err := s.view.loadPackages(cfg, query...)
pkgs, err := packages.Load(cfg, query...)
// If the context was canceled, return early. Otherwise, we might be
// type-checking an incomplete result. Check the context directly,

View File

@ -20,6 +20,7 @@ import (
"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"
"golang.org/x/tools/internal/telemetry/event"
errors "golang.org/x/xerrors"
@ -220,7 +221,7 @@ func goModWhy(ctx context.Context, cfg *packages.Config, folder string, data *mo
for _, req := range data.origParsedFile.Require {
inv.Args = append(inv.Args, req.Mod.Path)
}
stdout, err := inv.Run(ctx)
stdout, err := packagesinternal.GetGoCmdRunner(cfg).Run(ctx, inv)
if err != nil {
return err
}
@ -247,7 +248,7 @@ func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string
Env: cfg.Env,
WorkingDir: folder,
}
stdout, err := inv.Run(ctx)
stdout, err := packagesinternal.GetGoCmdRunner(cfg).Run(ctx, inv)
if err != nil {
return err
}
@ -288,6 +289,7 @@ func (s *snapshot) ModTidyHandle(ctx context.Context, realfh source.FileHandle)
cfg := s.Config(ctx)
options := s.View().Options()
folder := s.View().Folder().Filename()
gocmdRunner := s.view.gocmdRunner
wsPackages, err := s.WorkspacePackages(ctx)
if ctx.Err() != nil {
@ -358,14 +360,11 @@ func (s *snapshot) ModTidyHandle(ctx context.Context, realfh source.FileHandle)
Env: cfg.Env,
WorkingDir: folder,
}
if _, err := inv.Run(ctx); err != nil {
// Ignore concurrency errors here.
if !modConcurrencyError.MatchString(err.Error()) {
if _, err := gocmdRunner.Run(ctx, inv); err != nil {
return &modData{
err: err,
}
}
}
// Go directly to disk to get the temporary mod file, since it is always on disk.
tempContents, err := ioutil.ReadFile(tempURI.Filename())

View File

@ -11,6 +11,7 @@ import (
"sync"
"sync/atomic"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
@ -133,6 +134,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
modHandles: make(map[span.URI]*modHandle),
},
ignoredURIs: make(map[span.URI]struct{}),
gocmdRunner: &gocommand.Runner{},
}
v.snapshot.view = v

View File

@ -19,6 +19,7 @@ import (
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/event"
errors "golang.org/x/xerrors"
@ -94,7 +95,7 @@ func (s *snapshot) Config(ctx context.Context) *packages.Config {
verboseOutput := s.view.options.VerboseOutput
s.view.optionsMu.Unlock()
return &packages.Config{
cfg := &packages.Config{
Env: env,
Dir: s.view.folder.Filename(),
Context: ctx,
@ -117,6 +118,9 @@ func (s *snapshot) Config(ctx context.Context) *packages.Config {
},
Tests: true,
}
packagesinternal.SetGoCmdRunner(cfg, s.view.gocmdRunner)
return cfg
}
func (s *snapshot) buildOverlay() map[string][]byte {

View File

@ -15,12 +15,10 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"sync"
"time"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/debug"
@ -111,9 +109,8 @@ type view struct {
// `go env` variables that need to be tracked.
gopath, gocache string
// LoadMu guards packages.Load calls and associated state.
loadMu sync.Mutex
serializeLoads int
// gocmdRunner guards go command calls from concurrency errors.
gocmdRunner *gocommand.Runner
}
type builtinPackageHandle struct {
@ -279,7 +276,7 @@ func (v *view) WriteEnv(ctx context.Context, w io.Writer) error {
Env: env,
WorkingDir: v.Folder().Filename(),
}
stdout, err := inv.Run(ctx)
stdout, err := v.gocmdRunner.Run(ctx, inv)
if err != nil {
return err
}
@ -364,6 +361,7 @@ func (v *view) buildProcessEnv(ctx context.Context) (*imports.ProcessEnv, error)
WorkingDir: v.folder.Filename(),
BuildFlags: buildFlags,
LocalPrefix: localPrefix,
GocmdRunner: v.gocmdRunner,
}
if verboseOutput {
processEnv.Logf = func(format string, args ...interface{}) {
@ -734,7 +732,7 @@ func (v *view) getGoEnv(ctx context.Context, env []string) (string, error) {
Env: env,
WorkingDir: v.Folder().Filename(),
}
stdout, err := inv.Run(ctx)
stdout, err := v.gocmdRunner.Run(ctx, inv)
if err != nil {
return "", err
}
@ -767,50 +765,6 @@ func (v *view) getGoEnv(ctx context.Context, env []string) (string, error) {
return "", nil
}
// 1.13: go: updates to go.mod needed, but contents have changed
// 1.14: go: updating go.mod: existing contents have changed since last read
var modConcurrencyError = regexp.MustCompile(`go:.*go.mod.*contents have changed`)
// LoadPackages calls packages.Load, serializing requests if they fight over
// go.mod changes.
func (v *view) loadPackages(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) {
// We want to run go list calls concurrently as much as possible. However,
// if go.mod updates are needed, only one can make them and the others will
// fail. We need to retry in those cases, but we don't want to thrash so
// badly we never recover. To avoid that, once we've seen one concurrency
// error, start serializing everything until the backlog has cleared out.
// This could all be avoided on 1.14 by using multiple -modfiles.
v.loadMu.Lock()
var locked bool // If true, we hold the mutex and have incremented.
if v.serializeLoads == 0 {
v.loadMu.Unlock()
} else {
locked = true
v.serializeLoads++
}
defer func() {
if locked {
v.serializeLoads--
v.loadMu.Unlock()
}
}()
for {
pkgs, err := packages.Load(cfg, patterns...)
if err == nil || !modConcurrencyError.MatchString(err.Error()) {
return pkgs, err
}
event.Error(cfg.Context, "Load concurrency error, will retry serially", err)
if !locked {
v.loadMu.Lock()
v.serializeLoads++
locked = true
}
}
}
// This function will return the main go.mod file for this folder if it exists and whether the -modfile
// flag exists for this version of go.
func (v *view) modfileFlagExists(ctx context.Context, env []string) (bool, error) {
@ -824,7 +778,7 @@ func (v *view) modfileFlagExists(ctx context.Context, env []string) (bool, error
Env: append(env, "GO111MODULE=off"),
WorkingDir: v.Folder().Filename(),
}
stdout, err := inv.Run(ctx)
stdout, err := v.gocmdRunner.Run(ctx, inv)
if err != nil {
return false, err
}

View File

@ -11,6 +11,7 @@ import (
"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/packagesinternal"
"golang.org/x/tools/internal/xcontext"
errors "golang.org/x/xerrors"
)
@ -32,14 +33,16 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
if !ok {
return nil, err
}
cfg := snapshot.Config(ctx)
// Run go.mod tidy on the view.
inv := gocommand.Invocation{
Verb: "mod",
Args: []string{"tidy"},
Env: snapshot.Config(ctx).Env,
Env: cfg.Env,
WorkingDir: snapshot.View().Folder().Filename(),
}
if _, err := inv.Run(ctx); err != nil {
gocmdRunner := packagesinternal.GetGoCmdRunner(cfg)
if _, err := gocmdRunner.Run(ctx, inv); err != nil {
return nil, err
}
case "upgrade.dependency":
@ -52,14 +55,16 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
if !ok {
return nil, err
}
cfg := snapshot.Config(ctx)
// Run "go get" on the dependency to upgrade it to the latest version.
inv := gocommand.Invocation{
Verb: "get",
Args: strings.Split(deps, " "),
Env: snapshot.Config(ctx).Env,
Env: cfg.Env,
WorkingDir: snapshot.View().Folder().Filename(),
}
if _, err := inv.Run(ctx); err != nil {
gocmdRunner := packagesinternal.GetGoCmdRunner(cfg)
if _, err := gocmdRunner.Run(ctx, inv); err != nil {
return nil, err
}
}

View File

@ -50,10 +50,10 @@ func PrintVersionInfo(ctx context.Context, w io.Writer, verbose bool, mode Print
})
fmt.Fprint(w, "\n")
section(w, mode, "Go info", func() {
i := &gocommand.Invocation{
gocmdRunner := &gocommand.Runner{}
version, err := gocmdRunner.Run(ctx, gocommand.Invocation{
Verb: "version",
}
version, err := i.Run(ctx)
})
if err != nil {
panic(err)
}

View File

@ -157,7 +157,8 @@ func (w *Workspace) RunGoCommand(ctx context.Context, verb string, args ...strin
Args: args,
WorkingDir: w.workdir,
}
_, stderr, _, err := inv.RunRaw(ctx)
gocmdRunner := &gocommand.Runner{}
_, stderr, _, err := gocmdRunner.RunRaw(ctx, inv)
if err != nil {
return err
}

View File

@ -1,7 +1,11 @@
// Package packagesinternal exposes internal-only fields from go/packages.
package packagesinternal
import "time"
import (
"time"
"golang.org/x/tools/internal/gocommand"
)
// Fields must match go list;
type Module struct {
@ -25,3 +29,7 @@ type ModuleError struct {
var GetForTest = func(p interface{}) string { return "" }
var GetModule = func(p interface{}) *Module { return nil }
var GetGoCmdRunner = func(config interface{}) *gocommand.Runner { return nil }
var SetGoCmdRunner = func(config interface{}, runner *gocommand.Runner) {}