Skip to content

Commit 86dd056

Browse files
committed
internal/modindex: fix nil panic, and other bugs
ReadIndex: when it fails to read a non-existent index, it must return an error, since it did not complete its task; (nil, nil) is not acceptable. Document this, and fix all callers to handle the ErrNotExist case specially if necessary. Also: - use a collision-resistant hash (SHA256, not CRC32) when choosing a unique filename for each GOMODCACHE value. - Remove the special test-only logic in modcacheState.getIndex to call modindex.Create and retry. That's the tests' problem. - Three integration tests needed rather subtle changes to add "happens before" edges to populate GOMODCACHE before building the index, and building the index before invoking OrganizeImports (since it is fundamentally asynchronous): 1 misc.TestGOMODCACHE 2 misc.TestRelativeReplace (renamed to TestIssue67156 since the old name was a red herring) 3 diagnostics.Test_issue38211 The purpose of each of the steps in the three tests was also clarified. - Fix a bug in WithProxy: if the module cache is derived from a test's TempDir, it may contain "#" which has special meaning in GOPROXY. Now we report an error. The workaround is to use a fresh subtest, whose name will not contain "#". - Fix a bug in Sandbox.Close when it is called on a partially constructed sandbox. - Move CleanModCache into env package so it can be shared. Also flag a couple of related problems in comments that refer to it. - Link to new issues for existing unsolved mysteries encountered during testing. - Eliminate Create from public API; it's not needed. - Make Update and Read return the current index; this avoids the need for redundant Update+Read sequences. - modcacheindex no longer returns (nil, nil). - len(Entries)==0 is no longer a special case. - fix bug in modcacheTimed where len(new.Entries)==0 was erroneously used to mean "no change". - Rename Index.Changed to ValidAt, since it doesn't mean anything was actually changed at that time. - Record only the "best" directory for each import path, instead of a list, which must then be sorted. - Don't store symbols in "directory" type; it's now just an immutable triple value. - Ensure that modcacheState.getIndex updates s.index. The lack of updates in the previous code looked like an oversight. - Simplify "region" type away. - Log unexpected ReadIndex failures. - Rename dir, cachedir -> gomodcache throughout for clarity. - Eliminate an unnecessary path segment from IndexDir, and initialize it inline. - Better error hygiene. - Improve variable names, and eliminate unnecessary ones. - Improve commentary. - Make tests external, expressed using public API. Updates golang/go#74055 Change-Id: I3648d2cbd0dc3e169564f6c0dc122d223966b4c0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/682717 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent c37080d commit 86dd056

File tree

21 files changed

+603
-549
lines changed

21 files changed

+603
-549
lines changed

gopls/internal/cache/imports.go

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ package cache
77
import (
88
"context"
99
"fmt"
10+
"log"
1011
"sync"
11-
"testing"
1212
"time"
1313

1414
"golang.org/x/tools/gopls/internal/file"
@@ -150,66 +150,55 @@ func newImportsState(backgroundCtx context.Context, modCache *sharedModCache, en
150150

151151
// modcacheState holds a modindex.Index and controls its updates
152152
type modcacheState struct {
153-
dir string // GOMODCACHE
153+
gomodcache string
154154
refreshTimer *refreshTimer
155-
mu sync.Mutex
156-
index *modindex.Index
155+
156+
// (index, indexErr) is zero in the initial state.
157+
// Thereafter they hold the memoized result pair for getIndex.
158+
mu sync.Mutex
159+
index *modindex.Index
160+
indexErr error
157161
}
158162

159163
// newModcacheState constructs a new modcacheState for goimports.
160164
// The returned state is automatically updated until [modcacheState.stopTimer] is called.
161-
func newModcacheState(dir string) *modcacheState {
165+
func newModcacheState(gomodcache string) *modcacheState {
162166
s := &modcacheState{
163-
dir: dir,
167+
gomodcache: gomodcache,
164168
}
165-
s.index, _ = modindex.ReadIndex(dir)
166169
s.refreshTimer = newRefreshTimer(s.refreshIndex)
167170
go s.refreshIndex()
168171
return s
169172
}
170173

171-
// getIndex reads the module cache index. It might not exist yet
172-
// inside tests. It might contain no Entries if the cache
173-
// is empty.
174+
// getIndex reads the module cache index.
174175
func (s *modcacheState) getIndex() (*modindex.Index, error) {
175176
s.mu.Lock()
176177
defer s.mu.Unlock()
177-
ix := s.index
178-
if ix == nil || len(ix.Entries) == 0 {
179-
var err error
180-
// this should only happen near the beginning of a session
181-
// (or in tests)
182-
ix, err = modindex.ReadIndex(s.dir)
183-
if err != nil {
184-
return nil, fmt.Errorf("ReadIndex %w", err)
185-
}
186-
if !testing.Testing() {
187-
return ix, nil
188-
}
189-
if ix == nil || len(ix.Entries) == 0 {
190-
err = modindex.Create(s.dir)
191-
if err != nil {
192-
return nil, fmt.Errorf("creating index %w", err)
193-
}
194-
ix, err = modindex.ReadIndex(s.dir)
195-
if err != nil {
196-
return nil, fmt.Errorf("read index after create %w", err)
197-
}
198-
s.index = ix
199-
}
178+
179+
if s.index == nil && s.indexErr == nil {
180+
// getIndex was called before refreshIndex finished.
181+
// Read, but don't update, whatever index is present.
182+
s.index, s.indexErr = modindex.Read(s.gomodcache)
200183
}
201-
return s.index, nil
184+
185+
return s.index, s.indexErr
202186
}
203187

204188
func (s *modcacheState) refreshIndex() {
205-
ok, err := modindex.Update(s.dir)
206-
if err != nil || !ok {
207-
return
208-
}
209-
// read the new index
189+
index, err := modindex.Update(s.gomodcache)
210190
s.mu.Lock()
211-
defer s.mu.Unlock()
212-
s.index, _ = modindex.ReadIndex(s.dir)
191+
if err != nil {
192+
if s.indexErr != nil {
193+
s.indexErr = err // prefer most recent error
194+
} else {
195+
// Keep using stale s.index (if any).
196+
log.Printf("modcacheState.refreshIndex: %v", err)
197+
}
198+
} else {
199+
s.index, s.indexErr = index, nil // success
200+
}
201+
s.mu.Unlock()
213202
}
214203

215204
func (s *modcacheState) stopTimer() {

gopls/internal/cache/source.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
// goplsSource is an imports.Source that provides import information using
2222
// gopls and the module cache index.
2323
type goplsSource struct {
24-
S *Snapshot
24+
snapshot *Snapshot
2525
envSource *imports.ProcessEnvSource
2626

2727
// set by each invocation of ResolveReferences
@@ -30,7 +30,7 @@ type goplsSource struct {
3030

3131
func (s *Snapshot) NewGoplsSource(is *imports.ProcessEnvSource) *goplsSource {
3232
return &goplsSource{
33-
S: s,
33+
snapshot: s,
3434
envSource: is,
3535
}
3636
}
@@ -110,7 +110,7 @@ func (s *goplsSource) ResolveReferences(ctx context.Context, filename string, mi
110110

111111
dbgpr("fromWS", fromWS)
112112
dbgpr("old", old)
113-
for k, v := range s.S.workspacePackages.All() {
113+
for k, v := range s.snapshot.workspacePackages.All() {
114114
log.Printf("workspacePackages[%s]=%s", k, v)
115115
}
116116
// anything in ans with >1 matches?
@@ -134,7 +134,7 @@ func (s *goplsSource) ResolveReferences(ctx context.Context, filename string, mi
134134
}
135135

136136
func (s *goplsSource) resolveCacheReferences(missing imports.References) ([]*result, error) {
137-
ix, err := s.S.view.ModcacheIndex()
137+
ix, err := s.snapshot.view.ModcacheIndex()
138138
if err != nil {
139139
return nil, err
140140
}
@@ -176,7 +176,7 @@ type found struct {
176176

177177
func (s *goplsSource) resolveWorkspaceReferences(filename string, missing imports.References) ([]*imports.Result, error) {
178178
uri := protocol.URIFromPath(filename)
179-
mypkgs, err := s.S.MetadataForFile(s.ctx, uri)
179+
mypkgs, err := s.snapshot.MetadataForFile(s.ctx, uri)
180180
if err != nil {
181181
return nil, err
182182
}
@@ -185,7 +185,7 @@ func (s *goplsSource) resolveWorkspaceReferences(filename string, missing import
185185
}
186186
mypkg := mypkgs[0] // narrowest package
187187
// search the metadata graph for package ids correstponding to missing
188-
g := s.S.MetadataGraph()
188+
g := s.snapshot.MetadataGraph()
189189
var ids []metadata.PackageID
190190
var pkgs []*metadata.Package
191191
for pid, pkg := range g.Packages {
@@ -200,7 +200,7 @@ func (s *goplsSource) resolveWorkspaceReferences(filename string, missing import
200200
}
201201
// find the symbols in those packages
202202
// the syms occur in the same order as the ids and the pkgs
203-
syms, err := s.S.Symbols(s.ctx, ids...)
203+
syms, err := s.snapshot.Symbols(s.ctx, ids...)
204204
if err != nil {
205205
return nil, err
206206
}
@@ -331,12 +331,12 @@ func (s *goplsSource) bestCache(nm string, got []*result) *imports.Result {
331331
func (s *goplsSource) fromGoMod(got []*result) *imports.Result {
332332
// should we use s.S.view.worsspaceModFiles, and the union of their requires?
333333
// (note that there are no tests where it contains more than one)
334-
modURI := s.S.view.gomod
335-
modfh, ok := s.S.files.get(modURI)
334+
modURI := s.snapshot.view.gomod
335+
modfh, ok := s.snapshot.files.get(modURI)
336336
if !ok {
337337
return nil
338338
}
339-
parsed, err := s.S.ParseMod(s.ctx, modfh)
339+
parsed, err := s.snapshot.ParseMod(s.ctx, modfh)
340340
if err != nil {
341341
return nil
342342
}

gopls/internal/golang/completion/unimported.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,6 @@ func (c *completer) modcacheMatches(pkg metadata.PackageName, prefix string) ([]
224224
if err != nil {
225225
return nil, err
226226
}
227-
if ix == nil || len(ix.Entries) == 0 { // in tests ix might always be nil
228-
return nil, fmt.Errorf("no index %w", err)
229-
}
230227
// retrieve everything and let usefulCompletion() and the matcher sort them out
231228
cands := ix.Lookup(string(pkg), "", true)
232229
lx := len(cands)

gopls/internal/golang/format.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ func computeImportEdits(ctx context.Context, pgf *parsego.File, snapshot *cache.
149149
source = isource
150150
}
151151
// imports require a current metadata graph
152-
// TODO(rfindlay) improve the API
153-
snapshot.WorkspaceMetadata(ctx)
152+
// TODO(rfindley): improve the API
153+
snapshot.WorkspaceMetadata(ctx) // ignore error
154154
allFixes, err := imports.FixImports(ctx, filename, pgf.Src, goroot, options.Env.Logf, source)
155155
if err != nil {
156156
return nil, nil, err

gopls/internal/test/integration/bench/unimported_test.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,12 @@ func findSym(t testing.TB) (pkg, name, gomodcache string) {
123123
t.Fatal(err)
124124
}
125125
modcache := strings.TrimSpace(string(out))
126-
ix, err := modindex.ReadIndex(modcache)
126+
ix, err := modindex.Read(modcache)
127127
if err != nil {
128128
t.Fatal(err)
129129
}
130130
if ix == nil {
131-
t.Fatal("no index")
132-
}
133-
if len(ix.Entries) == 0 {
134-
t.Fatal("no entries")
131+
t.Fatal("nil index")
135132
}
136133
nth := 100 // or something
137134
for _, e := range ix.Entries {

gopls/internal/test/integration/diagnostics/diagnostics_test.go

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ var ErrHelpWanted error
681681
`
682682

683683
// Test for golang/go#38211.
684-
func Test_Issue38211(t *testing.T) {
684+
func Test_issue38211(t *testing.T) {
685685
const ardanLabs = `
686686
-- go.mod --
687687
module mod.com
@@ -696,46 +696,89 @@ func main() {
696696
_ = conf.ErrHelpWanted
697697
}
698698
`
699-
WithOptions(
699+
700+
modcache := t.TempDir()
701+
defer CleanModCache(t, modcache)
702+
703+
opts := []RunOption{
704+
EnvVars{"GOMODCACHE": modcache},
700705
ProxyFiles(ardanLabsProxy),
701-
).Run(t, ardanLabs, func(t *testing.T, env *Env) {
702-
// Expect a diagnostic with a suggested fix to add
703-
// "github.com/ardanlabs/conf" to the go.mod file.
706+
//WriteGoSum("."), // TODO(golang/go#74594): uncommenting this causes mysterious failure; investigate and make the error clearer (go list?)
707+
}
708+
709+
t.Run("setup", func(t *testing.T) {
710+
// Forcibly populate GOMODCACHE
711+
// so OrganizeImports can later rely on it.
712+
WithOptions(opts...).Run(t, ardanLabs, func(t *testing.T, env *Env) {
713+
// TODO(adonovan): why doesn't RunGoCommand respect EnvVars??
714+
// (That was the motivation to use Sandbox.RunGoCommand
715+
// rather than execute go mod download directly!)
716+
// See comment at CleanModCache and golang/go#74595.
717+
environ := []string{"GOMODCACHE=" + modcache}
718+
_, err := env.Sandbox.RunGoCommand(env.Ctx, "", "get", []string{"github.com/ardanlabs/[email protected]"}, environ, false)
719+
if err != nil {
720+
t.Error(err)
721+
}
722+
})
723+
})
724+
725+
WithOptions(opts...).Run(t, ardanLabs, func(t *testing.T, env *Env) {
726+
// Expect a "no module provides package" diagnostic.
704727
env.OpenFile("go.mod")
705728
env.OpenFile("main.go")
706729
var d protocol.PublishDiagnosticsParams
707730
env.AfterChange(
708-
Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)),
731+
Diagnostics(
732+
env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`),
733+
WithMessage("no required module provides package")),
709734
ReadDiagnostics("main.go", &d),
710735
)
736+
737+
// Apply the suggested fix to make the go.mod file
738+
// require "github.com/ardanlabs/conf".
711739
env.ApplyQuickFixes("main.go", d.Diagnostics)
712740
env.SaveBuffer("go.mod")
713741
env.AfterChange(
714742
NoDiagnostics(ForFile("main.go")),
715743
)
716-
// Comment out the line that depends on conf and expect a
717-
// diagnostic and a fix to remove the import.
744+
745+
// Comment out the sole use of conf,
746+
// causing an "unused import" diagnostic.
718747
env.RegexpReplace("main.go", "_ = conf.ErrHelpWanted", "//_ = conf.ErrHelpWanted")
719748
env.AfterChange(
720-
Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)),
749+
Diagnostics(
750+
env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`),
751+
WithMessage("imported and not used")),
721752
)
722-
env.SaveBuffer("main.go")
723-
// Expect a diagnostic and fix to remove the dependency in the go.mod.
753+
754+
// Remove the import using OrganizeImports, leading
755+
// to an "unused require" diagnostic in the go.mod.
756+
env.SaveBuffer("main.go") // => OrganizeImports
724757
env.AfterChange(
725758
NoDiagnostics(ForFile("main.go")),
726-
Diagnostics(env.AtRegexp("go.mod", "require github.com/ardanlabs/conf"), WithMessage("not used in this module")),
759+
Diagnostics(
760+
env.AtRegexp("go.mod", "require github.com/ardanlabs/conf"),
761+
WithMessage("not used in this module")),
727762
ReadDiagnostics("go.mod", &d),
728763
)
764+
765+
// Apply the suggested fix to remove the "require" directive.
729766
env.ApplyQuickFixes("go.mod", d.Diagnostics)
730767
env.SaveBuffer("go.mod")
731768
env.AfterChange(
732769
NoDiagnostics(ForFile("go.mod")),
733770
)
734-
// Uncomment the lines and expect a new diagnostic for the import.
771+
772+
// Uncomment the use of the import.
773+
// OrganizeImports should add the import.
774+
// Expect another "no required module provides package"
775+
// diagnostic, bringing us full circle.
735776
env.RegexpReplace("main.go", "//_ = conf.ErrHelpWanted", "_ = conf.ErrHelpWanted")
736-
env.SaveBuffer("main.go")
777+
env.SaveBuffer("main.go") // => OrganizeImports
737778
env.AfterChange(
738-
Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)),
779+
Diagnostics(
780+
env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`),
781+
WithMessage("no required module provides package")),
739782
)
740783
})
741784
}

gopls/internal/test/integration/env.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"context"
99
"fmt"
1010
"net/http/httptest"
11+
"os"
12+
"os/exec"
1113
"strings"
1214
"sync"
1315
"sync/atomic"
@@ -378,3 +380,20 @@ func indent(msg string) string {
378380
const prefix = " "
379381
return prefix + strings.ReplaceAll(msg, "\n", "\n"+prefix)
380382
}
383+
384+
// CleanModCache cleans the specified GOMODCACHE.
385+
//
386+
// TODO(golang/go#74595): this is only necessary as the module cache cleaning of the
387+
// sandbox does not respect GOMODCACHE set via EnvVars. We should fix this, but
388+
// that is probably part of a larger refactoring of the sandbox that I'm not
389+
// inclined to undertake. --rfindley.
390+
//
391+
// (For similar problems caused by the same bug, see Test_issue38211; see also
392+
// comment in Sandbox.Env.)
393+
func CleanModCache(t *testing.T, modcache string) {
394+
cmd := exec.Command("go", "clean", "-modcache")
395+
cmd.Env = append(os.Environ(), "GOMODCACHE="+modcache, "GOTOOLCHAIN=local")
396+
if output, err := cmd.CombinedOutput(); err != nil {
397+
t.Errorf("cleaning modcache: %v\noutput:\n%s", err, string(output))
398+
}
399+
}

gopls/internal/test/integration/fake/proxy.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package fake
66

77
import (
88
"fmt"
9+
"strings"
910

1011
"golang.org/x/tools/internal/proxydir"
1112
)
@@ -27,6 +28,11 @@ func WriteProxy(tmpdir string, files map[string][]byte) (string, error) {
2728
filesByModule[mv][suffix] = data
2829
}
2930
for mv, files := range filesByModule {
31+
// Don't hoist this check out of the loop:
32+
// the problem is benign if filesByModule is empty.
33+
if strings.Contains(tmpdir, "#") {
34+
return "", fmt.Errorf("WriteProxy's tmpdir contains '#', which is unsuitable for GOPROXY. (If tmpdir was derived from testing.T.Name, use t.Run to ensure that each subtest has a unique name.)")
35+
}
3036
if err := proxydir.WriteModuleVersion(tmpdir, mv.modulePath, mv.version, files); err != nil {
3137
return "", fmt.Errorf("error writing %s@%s: %v", mv.modulePath, mv.version, err)
3238
}

0 commit comments

Comments
 (0)