Skip to content

Commit f6dc1e9

Browse files
committed
internal/imports: merge init and newModuleResolver
Switch the ModuleResolver from a lazy initialization model to a constructor, so that the resolver returned by ProcessEnv.GetResolver is ready to use. A constructor is easier to maintain as it involves less state, avoids redundant calls to init, and avoids bugs where the call to init is missing (such as was the case for the scoreImportPath method). It also lets the caller differentiate between a failure to construct the resolver and a resolver that only returns errors. Pragmatically, I'd like to move toward a model where the caller re-scans imports by asynchronously cloning, priming, and replacing the resolver, rather than blocking. This is a step in that direction. This change is not without risk, but it looks like all calls to ModuleResolver methods are preceded by a call to GetResolver, and errors from GetResolver are handled similarly errors from methods. There is some messiness resulting from this change, particularly in tests. These are noted inline, and can eventually be fixed by similarly avoiding lazy initialization of ProcessEnv. For golang/go#59216 Change-Id: I3b7206417f61a5697ed83e811c177f646afce3b2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/559635 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 4f6024e commit f6dc1e9

File tree

4 files changed

+114
-101
lines changed

4 files changed

+114
-101
lines changed

gopls/internal/cache/imports.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,7 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot
5151
// update the processEnv. Clearing caches blocks on any background
5252
// scans.
5353
if modFileHash != s.cachedModFileHash {
54-
if resolver, err := s.processEnv.GetResolver(); err == nil {
55-
if modResolver, ok := resolver.(*imports.ModuleResolver); ok {
56-
modResolver.ClearForNewMod()
57-
}
58-
}
59-
54+
s.processEnv.ClearModuleInfo()
6055
s.cachedModFileHash = modFileHash
6156
}
6257

internal/imports/fix.go

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,21 @@ var requiredGoEnvVars = []string{
847847

848848
// ProcessEnv contains environment variables and settings that affect the use of
849849
// the go command, the go/build package, etc.
850+
//
851+
// ...a ProcessEnv *also* overwrites its Env along with derived state in the
852+
// form of the resolver. And because it is lazily initialized, an env may just
853+
// be broken and unusable, but there is no way for the caller to detect that:
854+
// all queries will just fail.
855+
//
856+
// TODO(rfindley): refactor this package so that this type (perhaps renamed to
857+
// just Env or Config) is an immutable configuration struct, to be exchanged
858+
// for an initialized object via a constructor that returns an error. Perhaps
859+
// the signature should be `func NewResolver(*Env) (*Resolver, error)`, where
860+
// resolver is a concrete type used for resolving imports. Via this
861+
// refactoring, we can avoid the need to call ProcessEnv.init and
862+
// ProcessEnv.GoEnv everywhere, and implicitly fix all the places where this
863+
// these are misused. Also, we'd delegate the caller the decision of how to
864+
// handle a broken environment.
850865
type ProcessEnv struct {
851866
GocmdRunner *gocommand.Runner
852867

@@ -869,11 +884,13 @@ type ProcessEnv struct {
869884
// If Logf is non-nil, debug logging is enabled through this function.
870885
Logf func(format string, args ...interface{})
871886

872-
// TODO(rfindley): for simplicity, use a constructor rather than
873-
// initialization pattern for ProcessEnv.
874-
initialized bool
887+
initialized bool // see TODO above
875888

876-
resolver Resolver
889+
// resolver and resolverErr are lazily evaluated (see GetResolver).
890+
// This is unclean, but see the big TODO in the docstring for ProcessEnv
891+
// above: for now, we can't be sure that the ProcessEnv is fully initialized.
892+
resolver Resolver
893+
resolverErr error
877894
}
878895

879896
func (e *ProcessEnv) goEnv() (map[string]string, error) {
@@ -953,24 +970,25 @@ func (e *ProcessEnv) env() []string {
953970
}
954971

955972
func (e *ProcessEnv) GetResolver() (Resolver, error) {
956-
if e.resolver != nil {
957-
return e.resolver, nil
958-
}
959973
if err := e.init(); err != nil {
960974
return nil, err
961975
}
962-
// TODO(rfindley): we should only use a gopathResolver here if the working
963-
// directory is actually *in* GOPATH. (I seem to recall an open gopls issue
964-
// for this behavior, but I can't find it).
965-
//
966-
// For gopls, we can optionally explicitly choose a resolver type, since we
967-
// already know the view type.
968-
if len(e.Env["GOMOD"]) == 0 && len(e.Env["GOWORK"]) == 0 {
969-
e.resolver = newGopathResolver(e)
970-
return e.resolver, nil
971-
}
972-
e.resolver = newModuleResolver(e)
973-
return e.resolver, nil
976+
977+
if e.resolver == nil && e.resolverErr == nil {
978+
// TODO(rfindley): we should only use a gopathResolver here if the working
979+
// directory is actually *in* GOPATH. (I seem to recall an open gopls issue
980+
// for this behavior, but I can't find it).
981+
//
982+
// For gopls, we can optionally explicitly choose a resolver type, since we
983+
// already know the view type.
984+
if len(e.Env["GOMOD"]) == 0 && len(e.Env["GOWORK"]) == 0 {
985+
e.resolver = newGopathResolver(e)
986+
} else {
987+
e.resolver, e.resolverErr = newModuleResolver(e)
988+
}
989+
}
990+
991+
return e.resolver, e.resolverErr
974992
}
975993

976994
// buildContext returns the build.Context to use for matching files.

internal/imports/mod.go

Lines changed: 50 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ import (
6262
type ModuleResolver struct {
6363
env *ProcessEnv
6464

65-
// Module state, populated by init
66-
initialized bool
65+
// Module state, populated during construction
6766
dummyVendorMod *gocommand.ModuleJSON // if vendoring is enabled, a pseudo-module to represent the /vendor directory
6867
moduleCacheDir string // GOMODCACHE, inferred from GOPATH if unset
6968
roots []gopathwalk.Root // roots to scan, in approximate order of importance
@@ -87,24 +86,18 @@ type ModuleResolver struct {
8786
otherCache *dirInfoCache
8887
}
8988

90-
func newModuleResolver(e *ProcessEnv) *ModuleResolver {
89+
func newModuleResolver(e *ProcessEnv) (*ModuleResolver, error) {
9190
r := &ModuleResolver{
9291
env: e,
9392
scanSema: make(chan struct{}, 1),
9493
}
9594
r.scanSema <- struct{}{}
96-
return r
97-
}
98-
99-
func (r *ModuleResolver) init() error {
100-
if r.initialized {
101-
return nil
102-
}
10395

10496
goenv, err := r.env.goEnv()
10597
if err != nil {
106-
return err
98+
return nil, err
10799
}
100+
108101
// TODO(rfindley): can we refactor to share logic with r.env.invokeGo?
109102
inv := gocommand.Invocation{
110103
BuildFlags: r.env.BuildFlags,
@@ -125,7 +118,7 @@ func (r *ModuleResolver) init() error {
125118
// invocation?
126119
vendorEnabled, mainModVendor, err = gocommand.VendorEnabled(context.TODO(), inv, r.env.GocmdRunner)
127120
if err != nil {
128-
return err
121+
return nil, err
129122
}
130123
}
131124

@@ -146,19 +139,14 @@ func (r *ModuleResolver) init() error {
146139
// GO111MODULE=on. Other errors are fatal.
147140
if err != nil {
148141
if errMsg := err.Error(); !strings.Contains(errMsg, "working directory is not part of a module") && !strings.Contains(errMsg, "go.mod file not found") {
149-
return err
142+
return nil, err
150143
}
151144
}
152145
}
153146

154-
if gmc := r.env.Env["GOMODCACHE"]; gmc != "" {
155-
r.moduleCacheDir = gmc
156-
} else {
157-
gopaths := filepath.SplitList(goenv["GOPATH"])
158-
if len(gopaths) == 0 {
159-
return fmt.Errorf("empty GOPATH")
160-
}
161-
r.moduleCacheDir = filepath.Join(gopaths[0], "/pkg/mod")
147+
r.moduleCacheDir = gomodcacheForEnv(goenv)
148+
if r.moduleCacheDir == "" {
149+
return nil, fmt.Errorf("cannot resolve GOMODCACHE")
162150
}
163151

164152
sort.Slice(r.modsByModPath, func(i, j int) bool {
@@ -212,20 +200,32 @@ func (r *ModuleResolver) init() error {
212200
}
213201

214202
r.scannedRoots = map[gopathwalk.Root]bool{}
215-
if r.moduleCacheCache == nil {
216-
r.moduleCacheCache = &dirInfoCache{
217-
dirs: map[string]*directoryPackageInfo{},
218-
listeners: map[*int]cacheListener{},
219-
}
203+
r.moduleCacheCache = &dirInfoCache{
204+
dirs: map[string]*directoryPackageInfo{},
205+
listeners: map[*int]cacheListener{},
220206
}
221-
if r.otherCache == nil {
222-
r.otherCache = &dirInfoCache{
223-
dirs: map[string]*directoryPackageInfo{},
224-
listeners: map[*int]cacheListener{},
225-
}
207+
r.otherCache = &dirInfoCache{
208+
dirs: map[string]*directoryPackageInfo{},
209+
listeners: map[*int]cacheListener{},
226210
}
227-
r.initialized = true
228-
return nil
211+
return r, nil
212+
}
213+
214+
// gomodcacheForEnv returns the GOMODCACHE value to use based on the given env
215+
// map, which must have GOMODCACHE and GOPATH populated.
216+
//
217+
// TODO(rfindley): this is defensive refactoring.
218+
// 1. Is this even relevant anymore? Can't we just read GOMODCACHE.
219+
// 2. Use this to separate module cache scanning from other scanning.
220+
func gomodcacheForEnv(goenv map[string]string) string {
221+
if gmc := goenv["GOMODCACHE"]; gmc != "" {
222+
return gmc
223+
}
224+
gopaths := filepath.SplitList(goenv["GOPATH"])
225+
if len(gopaths) == 0 {
226+
return ""
227+
}
228+
return filepath.Join(gopaths[0], "/pkg/mod")
229229
}
230230

231231
func (r *ModuleResolver) initAllMods() error {
@@ -271,21 +271,27 @@ func (r *ModuleResolver) ClearForNewScan() {
271271
r.scanSema <- struct{}{}
272272
}
273273

274-
// ClearForNewMod invalidates resolver state that depends on the go.mod file
275-
// (essentially, the output of go list -m -json ...).
274+
// ClearModuleInfo invalidates resolver state that depends on go.mod file
275+
// contents (essentially, the output of go list -m -json ...).
276276
//
277277
// Notably, it does not forget directory contents, which are reset
278278
// asynchronously via ClearForNewScan.
279-
func (r *ModuleResolver) ClearForNewMod() {
280-
<-r.scanSema
281-
*r = ModuleResolver{
282-
env: r.env,
283-
moduleCacheCache: r.moduleCacheCache,
284-
otherCache: r.otherCache,
285-
scanSema: r.scanSema,
279+
//
280+
// If the ProcessEnv is a GOPATH environment, ClearModuleInfo is a no op.
281+
//
282+
// TODO(rfindley): move this to a new env.go, consolidating ProcessEnv methods.
283+
func (e *ProcessEnv) ClearModuleInfo() {
284+
if r, ok := e.resolver.(*ModuleResolver); ok {
285+
resolver, resolverErr := newModuleResolver(e)
286+
if resolverErr == nil {
287+
<-r.scanSema // guards caches
288+
resolver.moduleCacheCache = r.moduleCacheCache
289+
resolver.otherCache = r.otherCache
290+
r.scanSema <- struct{}{}
291+
}
292+
e.resolver = resolver
293+
e.resolverErr = resolverErr
286294
}
287-
r.init()
288-
r.scanSema <- struct{}{}
289295
}
290296

291297
// findPackage returns the module and directory that contains the package at
@@ -355,10 +361,6 @@ func (r *ModuleResolver) cacheStore(info directoryPackageInfo) {
355361
}
356362
}
357363

358-
func (r *ModuleResolver) cacheKeys() []string {
359-
return append(r.moduleCacheCache.Keys(), r.otherCache.Keys()...)
360-
}
361-
362364
// cachePackageName caches the package name for a dir already in the cache.
363365
func (r *ModuleResolver) cachePackageName(info directoryPackageInfo) (string, error) {
364366
if info.rootType == gopathwalk.RootModuleCache {
@@ -469,9 +471,6 @@ func (r *ModuleResolver) dirInModuleCache(dir string) bool {
469471
}
470472

471473
func (r *ModuleResolver) loadPackageNames(importPaths []string, srcDir string) (map[string]string, error) {
472-
if err := r.init(); err != nil {
473-
return nil, err
474-
}
475474
names := map[string]string{}
476475
for _, path := range importPaths {
477476
_, packageDir := r.findPackage(path)
@@ -491,10 +490,6 @@ func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error
491490
ctx, done := event.Start(ctx, "imports.ModuleResolver.scan")
492491
defer done()
493492

494-
if err := r.init(); err != nil {
495-
return err
496-
}
497-
498493
processDir := func(info directoryPackageInfo) {
499494
// Skip this directory if we were not able to get the package information successfully.
500495
if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil {
@@ -672,9 +667,6 @@ func (r *ModuleResolver) canonicalize(info directoryPackageInfo) (*pkg, error) {
672667
}
673668

674669
func (r *ModuleResolver) loadExports(ctx context.Context, pkg *pkg, includeTest bool) (string, []string, error) {
675-
if err := r.init(); err != nil {
676-
return "", nil, err
677-
}
678670
if info, ok := r.cacheLoad(pkg.dir); ok && !includeTest {
679671
return r.cacheExports(ctx, r.env, info)
680672
}

0 commit comments

Comments
 (0)