gopls/internal/lsp/cache: pre-compute load diagnostics

Process go/packages errors immediately after loading, to eliminate a
dependency on syntax packages.

This simplifies the cache.Package type, which is now just a simple join
of metadata and type-checked syntax.

For golang/go#57987

Change-Id: I3d120fb4cb5687df2a376f8d8617e23e16ddaa92
Reviewed-on: https://go-review.googlesource.com/c/tools/+/468776
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index c6e6f74..edefa43 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -636,12 +636,12 @@
 // These may be attached to import declarations in the transitive source files
 // of pkg, or to 'requires' declarations in the package's go.mod file.
 //
-// TODO(rfindley): move this to errors.go
-func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsErrors []*packagesinternal.PackageError) ([]*source.Diagnostic, error) {
+// TODO(rfindley): move this to load.go
+func depsErrors(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs source.FileSource, workspacePackages map[PackageID]PackagePath) ([]*source.Diagnostic, error) {
 	// Select packages that can't be found, and were imported in non-workspace packages.
 	// Workspace packages already show their own errors.
 	var relevantErrors []*packagesinternal.PackageError
-	for _, depsError := range depsErrors {
+	for _, depsError := range m.DepsErrors {
 		// Up to Go 1.15, the missing package was included in the stack, which
 		// was presumably a bug. We want the next one up.
 		directImporterIdx := len(depsError.ImportStack) - 1
@@ -650,7 +650,7 @@
 		}
 
 		directImporter := depsError.ImportStack[directImporterIdx]
-		if s.isWorkspacePackage(PackageID(directImporter)) {
+		if _, ok := workspacePackages[PackageID(directImporter)]; ok {
 			continue
 		}
 		relevantErrors = append(relevantErrors, depsError)
@@ -661,21 +661,31 @@
 		return nil, nil
 	}
 
+	// Subsequent checks require Go files.
+	if len(m.CompiledGoFiles) == 0 {
+		return nil, nil
+	}
+
 	// Build an index of all imports in the package.
 	type fileImport struct {
 		cgf *source.ParsedGoFile
 		imp *ast.ImportSpec
 	}
 	allImports := map[string][]fileImport{}
-	for _, cgf := range pkg.compiledGoFiles {
+	for _, uri := range m.CompiledGoFiles {
+		pgf, err := parseGoURI(ctx, fs, uri, source.ParseHeader)
+		if err != nil {
+			return nil, err
+		}
+		fset := source.SingletonFileSet(pgf.Tok)
 		// TODO(adonovan): modify Imports() to accept a single token.File (cgf.Tok).
-		for _, group := range astutil.Imports(pkg.fset, cgf.File) {
+		for _, group := range astutil.Imports(fset, pgf.File) {
 			for _, imp := range group {
 				if imp.Path == nil {
 					continue
 				}
 				path := strings.Trim(imp.Path.Value, `"`)
-				allImports[path] = append(allImports[path], fileImport{cgf, imp})
+				allImports[path] = append(allImports[path], fileImport{pgf, imp})
 			}
 		}
 	}
@@ -686,7 +696,7 @@
 	for _, depErr := range relevantErrors {
 		for i := len(depErr.ImportStack) - 1; i >= 0; i-- {
 			item := depErr.ImportStack[i]
-			if s.isWorkspacePackage(PackageID(item)) {
+			if _, ok := workspacePackages[PackageID(item)]; ok {
 				break
 			}
 
@@ -695,7 +705,7 @@
 				if err != nil {
 					return nil, err
 				}
-				fixes, err := goGetQuickFixes(s, imp.cgf.URI, item)
+				fixes, err := goGetQuickFixes(m.Module != nil, imp.cgf.URI, item)
 				if err != nil {
 					return nil, err
 				}
@@ -711,18 +721,11 @@
 		}
 	}
 
-	if len(pkg.compiledGoFiles) == 0 {
-		return errors, nil
-	}
-	mod := s.GoModForFile(pkg.compiledGoFiles[0].URI)
-	if mod == "" {
-		return errors, nil
-	}
-	fh, err := s.GetFile(ctx, mod)
+	modFile, err := nearestModFile(ctx, m.CompiledGoFiles[0], fs)
 	if err != nil {
 		return nil, err
 	}
-	pm, err := s.ParseMod(ctx, fh)
+	pm, err := parseModURI(ctx, fs, modFile)
 	if err != nil {
 		return nil, err
 	}
@@ -732,7 +735,7 @@
 	for _, depErr := range relevantErrors {
 		for i := len(depErr.ImportStack) - 1; i >= 0; i-- {
 			item := depErr.ImportStack[i]
-			m := s.Metadata(PackageID(item))
+			m := meta.metadata[PackageID(item)]
 			if m == nil || m.Module == nil {
 				continue
 			}
@@ -745,7 +748,7 @@
 			if err != nil {
 				return nil, err
 			}
-			fixes, err := goGetQuickFixes(s, pm.URI, item)
+			fixes, err := goGetQuickFixes(true, pm.URI, item)
 			if err != nil {
 				return nil, err
 			}
diff --git a/gopls/internal/lsp/cache/debug.go b/gopls/internal/lsp/cache/debug.go
index fd82aff..9d9de63 100644
--- a/gopls/internal/lsp/cache/debug.go
+++ b/gopls/internal/lsp/cache/debug.go
@@ -30,6 +30,8 @@
 
 // If debugEnabled is true, dumpWorkspace prints a summary of workspace
 // packages to stderr. If debugEnabled is false, it is a no-op.
+//
+// TODO(rfindley): this has served its purpose. Delete.
 func (s *snapshot) dumpWorkspace(context string) {
 	if !debugEnabled {
 		return
diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go
index 9585cf4..2d21705 100644
--- a/gopls/internal/lsp/cache/errors.go
+++ b/gopls/internal/lsp/cache/errors.go
@@ -9,8 +9,10 @@
 // source.Diagnostic form, and suggesting quick fixes.
 
 import (
+	"context"
 	"fmt"
 	"go/scanner"
+	"go/token"
 	"go/types"
 	"log"
 	"regexp"
@@ -28,20 +30,26 @@
 	"golang.org/x/tools/internal/typesinternal"
 )
 
-func goPackagesErrorDiagnostics(e packages.Error, pkg *syntaxPackage, fromDir string) (diags []*source.Diagnostic, rerr error) {
-	if diag, ok := parseGoListImportCycleError(e, pkg); ok {
+// goPackagesErrorDiagnostics translates the given go/packages Error into a
+// diagnostic, using the provided metadata and filesource.
+//
+// The slice of diagnostics may be empty.
+func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) ([]*source.Diagnostic, error) {
+	if diag, err := parseGoListImportCycleError(ctx, e, m, fs); err != nil {
+		return nil, err
+	} else if diag != nil {
 		return []*source.Diagnostic{diag}, nil
 	}
 
 	var spn span.Span
 	if e.Pos == "" {
-		spn = parseGoListError(e.Msg, fromDir)
+		spn = parseGoListError(e.Msg, m.LoadDir)
 		// We may not have been able to parse a valid span. Apply the errors to all files.
-		if _, err := spanToRange(pkg, spn); err != nil {
+		if _, err := spanToRange(ctx, fs, spn); err != nil {
 			var diags []*source.Diagnostic
-			for _, pgf := range pkg.compiledGoFiles {
+			for _, uri := range m.CompiledGoFiles {
 				diags = append(diags, &source.Diagnostic{
-					URI:      pgf.URI,
+					URI:      uri,
 					Severity: protocol.SeverityError,
 					Source:   source.ListError,
 					Message:  e.Msg,
@@ -50,7 +58,7 @@
 			return diags, nil
 		}
 	} else {
-		spn = span.ParseInDir(e.Pos, fromDir)
+		spn = span.ParseInDir(e.Pos, m.LoadDir)
 	}
 
 	// TODO(rfindley): in some cases the go command outputs invalid spans, for
@@ -64,7 +72,7 @@
 	// likely because *token.File lacks information about newline termination.
 	//
 	// We could do better here by handling that case.
-	rng, err := spanToRange(pkg, spn)
+	rng, err := spanToRange(ctx, fs, spn)
 	if err != nil {
 		return nil, err
 	}
@@ -136,7 +144,7 @@
 	}
 
 	if match := importErrorRe.FindStringSubmatch(e.primary.Msg); match != nil {
-		diag.SuggestedFixes, err = goGetQuickFixes(snapshot, loc.URI.SpanURI(), match[1])
+		diag.SuggestedFixes, err = goGetQuickFixes(snapshot.moduleMode(), loc.URI.SpanURI(), match[1])
 		if err != nil {
 			return nil, err
 		}
@@ -150,9 +158,8 @@
 	return []*source.Diagnostic{diag}, nil
 }
 
-func goGetQuickFixes(snapshot *snapshot, uri span.URI, pkg string) ([]source.SuggestedFix, error) {
-	// Go get only supports module mode for now.
-	if snapshot.workspaceMode()&moduleMode == 0 {
+func goGetQuickFixes(moduleMode bool, uri span.URI, pkg string) ([]source.SuggestedFix, error) {
+	if !moduleMode {
 		return nil, nil
 	}
 	title := fmt.Sprintf("go get package %v", pkg)
@@ -312,14 +319,20 @@
 	return ecode, loc, err
 }
 
-// spanToRange converts a span.Span to a protocol.Range,
-// assuming that the span belongs to the package whose diagnostics are being computed.
-func spanToRange(pkg *syntaxPackage, spn span.Span) (protocol.Range, error) {
-	pgf, err := pkg.File(spn.URI())
+// spanToRange converts a span.Span to a protocol.Range, by mapping content
+// contained in the provided FileSource.
+func spanToRange(ctx context.Context, fs source.FileSource, spn span.Span) (protocol.Range, error) {
+	uri := spn.URI()
+	fh, err := fs.GetFile(ctx, uri)
 	if err != nil {
 		return protocol.Range{}, err
 	}
-	return pgf.Mapper.SpanRange(spn)
+	content, err := fh.Read()
+	if err != nil {
+		return protocol.Range{}, err
+	}
+	mapper := protocol.NewMapper(uri, content)
+	return mapper.SpanRange(spn)
 }
 
 // parseGoListError attempts to parse a standard `go list` error message
@@ -338,28 +351,36 @@
 	return span.ParseInDir(input[:msgIndex], wd)
 }
 
-func parseGoListImportCycleError(e packages.Error, pkg *syntaxPackage) (*source.Diagnostic, bool) {
+// parseGoListImportCycleError attempts to parse the given go/packages error as
+// an import cycle, returning a diagnostic if successful.
+//
+// If the error is not detected as an import cycle error, it returns nil, nil.
+func parseGoListImportCycleError(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) (*source.Diagnostic, error) {
 	re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`)
 	matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg))
 	if len(matches) < 3 {
-		return nil, false
+		return nil, nil
 	}
 	msg := matches[1]
 	importList := strings.Split(matches[2], " ")
 	// Since the error is relative to the current package. The import that is causing
 	// the import cycle error is the second one in the list.
 	if len(importList) < 2 {
-		return nil, false
+		return nil, nil
 	}
 	// Imports have quotation marks around them.
 	circImp := strconv.Quote(importList[1])
-	for _, pgf := range pkg.compiledGoFiles {
+	for _, uri := range m.CompiledGoFiles {
+		pgf, err := parseGoURI(ctx, fs, uri, source.ParseHeader)
+		if err != nil {
+			return nil, err
+		}
 		// Search file imports for the import that is causing the import cycle.
 		for _, imp := range pgf.File.Imports {
 			if imp.Path.Value == circImp {
 				rng, err := pgf.NodeMappedRange(imp)
 				if err != nil {
-					return nil, false
+					return nil, nil
 				}
 
 				return &source.Diagnostic{
@@ -368,9 +389,35 @@
 					Severity: protocol.SeverityError,
 					Source:   source.ListError,
 					Message:  msg,
-				}, true
+				}, nil
 			}
 		}
 	}
-	return nil, false
+	return nil, nil
+}
+
+// parseGoURI is a helper to parse the Go file at the given URI from the file
+// source fs. The resulting syntax and token.File belong to an ephemeral,
+// encapsulated FileSet, so this file stands only on its own: it's not suitable
+// to use in a list of file of a package, for example.
+//
+// It returns an error if the file could not be read.
+func parseGoURI(ctx context.Context, fs source.FileSource, uri span.URI, mode source.ParseMode) (*source.ParsedGoFile, error) {
+	fh, err := fs.GetFile(ctx, uri)
+	if err != nil {
+		return nil, err
+	}
+	return parseGoImpl(ctx, token.NewFileSet(), fh, source.ParseHeader)
+}
+
+// parseModURI is a helper to parse the Mod file at the given URI from the file
+// source fs.
+//
+// It returns an error if the file could not be read.
+func parseModURI(ctx context.Context, fs source.FileSource, uri span.URI) (*source.ParsedModule, error) {
+	fh, err := fs.GetFile(ctx, uri)
+	if err != nil {
+		return nil, err
+	}
+	return parseModImpl(ctx, fh)
 }
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 98a8fec..22af16c 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -229,10 +229,20 @@
 
 	event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates)))
 
-	s.meta = s.meta.Clone(updates)
-	s.resetIsActivePackageLocked()
+	// Before mutating the snapshot, ensure that we compute load diagnostics
+	// successfully. This could fail if the context is cancelled, and we don't
+	// want to leave the snapshot metadata in a partial state.
+	meta := s.meta.Clone(updates)
+	workspacePackages := computeWorkspacePackagesLocked(s, meta)
+	for _, update := range updates {
+		if err := computeLoadDiagnostics(ctx, update, meta, lockedSnapshot{s}, workspacePackages); err != nil {
+			return err
+		}
+	}
+	s.meta = meta
+	s.workspacePackages = workspacePackages
 
-	s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta)
+	s.resetIsActivePackageLocked()
 	s.dumpWorkspace("load")
 	s.mu.Unlock()
 
@@ -549,6 +559,46 @@
 	m.DepsByImpPath = depsByImpPath
 	m.DepsByPkgPath = depsByPkgPath
 
+	// m.Diagnostics is set later in the loading pass, using
+	// computeLoadDiagnostics.
+
+	return nil
+}
+
+// computeLoadDiagnostics computes and sets m.Diagnostics for the given metadata m.
+//
+// It should only be called during metadata construction in snapshot.load.
+func computeLoadDiagnostics(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs source.FileSource, workspacePackages map[PackageID]PackagePath) error {
+	for _, packagesErr := range m.Errors {
+		// Filter out parse errors from go list. We'll get them when we
+		// actually parse, and buggy overlay support may generate spurious
+		// errors. (See TestNewModule_Issue38207.)
+		if strings.Contains(packagesErr.Msg, "expected '") {
+			continue
+		}
+		pkgDiags, err := goPackagesErrorDiagnostics(ctx, packagesErr, m, fs)
+		if err != nil {
+			// There are certain cases where the go command returns invalid
+			// positions, so we cannot panic or even bug.Reportf here.
+			event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(m.ID)))
+			continue
+		}
+		m.Diagnostics = append(m.Diagnostics, pkgDiags...)
+	}
+
+	// TODO(rfindley): this is buggy: an insignificant change to a modfile
+	// (or an unsaved modfile) could affect the position of deps errors,
+	// without invalidating the package.
+	depsDiags, err := depsErrors(ctx, m, meta, fs, workspacePackages)
+	if err != nil {
+		if ctx.Err() == nil {
+			// TODO(rfindley): consider making this a bug.Reportf. depsErrors should
+			// not normally fail.
+			event.Error(ctx, "unable to compute deps errors", err, tag.Package.Of(string(m.ID)))
+		}
+		return nil
+	}
+	m.Diagnostics = append(m.Diagnostics, depsDiags...)
 	return nil
 }
 
diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go
index 8c45948..76060ef 100644
--- a/gopls/internal/lsp/cache/pkg.go
+++ b/gopls/internal/lsp/cache/pkg.go
@@ -11,7 +11,6 @@
 	"go/scanner"
 	"go/token"
 	"go/types"
-	"strings"
 
 	"golang.org/x/tools/go/types/objectpath"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -19,8 +18,6 @@
 	"golang.org/x/tools/gopls/internal/lsp/source/methodsets"
 	"golang.org/x/tools/gopls/internal/lsp/source/xrefs"
 	"golang.org/x/tools/gopls/internal/span"
-	"golang.org/x/tools/internal/event"
-	"golang.org/x/tools/internal/event/tag"
 	"golang.org/x/tools/internal/memoize"
 )
 
@@ -39,54 +36,8 @@
 // loadDiagnostics, because the value of the snapshot.packages map  is just the
 // package handle. Fix this.
 type Package struct {
-	m               *source.Metadata
-	pkg             *syntaxPackage
-	loadDiagnostics *memoize.Promise // post-processed errors from loading
-}
-
-func newPackage(m *source.Metadata, pkg *syntaxPackage) *Package {
-	p := &Package{
-		m:   m,
-		pkg: pkg,
-	}
-	if len(m.Errors) > 0 || len(m.DepsErrors) > 0 {
-		p.loadDiagnostics = memoize.NewPromise(fmt.Sprintf("loadDiagnostics(%s)", m.ID), func(ctx context.Context, arg interface{}) interface{} {
-			s := arg.(*snapshot)
-			var diags []*source.Diagnostic
-			for _, packagesErr := range p.m.Errors {
-				// Filter out parse errors from go list. We'll get them when we
-				// actually parse, and buggy overlay support may generate spurious
-				// errors. (See TestNewModule_Issue38207.)
-				if strings.Contains(packagesErr.Msg, "expected '") {
-					continue
-				}
-				pkgDiags, err := goPackagesErrorDiagnostics(packagesErr, p.pkg, p.m.LoadDir)
-				if err != nil {
-					// There are certain cases where the go command returns invalid
-					// positions, so we cannot panic or even bug.Reportf here.
-					event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(p.m.ID)))
-					continue
-				}
-				diags = append(diags, pkgDiags...)
-			}
-
-			// TODO(rfindley): this is buggy: an insignificant change to a modfile
-			// (or an unsaved modfile) could affect the position of deps errors,
-			// without invalidating the package.
-			depsDiags, err := s.depsErrors(ctx, p.pkg, p.m.DepsErrors)
-			if err != nil {
-				if ctx.Err() == nil {
-					// TODO(rfindley): consider making this a bug.Reportf. depsErrors should
-					// not normally fail.
-					event.Error(ctx, "unable to compute deps errors", err, tag.Package.Of(string(p.m.ID)))
-				}
-				return nil
-			}
-			diags = append(diags, depsDiags...)
-			return diags
-		})
-	}
-	return p
+	m   *source.Metadata
+	pkg *syntaxPackage
 }
 
 // syntaxPackage contains parse trees and type information for a package.
@@ -235,21 +186,14 @@
 
 func (p *Package) DiagnosticsForFile(ctx context.Context, s source.Snapshot, uri span.URI) ([]*source.Diagnostic, error) {
 	var diags []*source.Diagnostic
-	for _, diag := range p.pkg.diagnostics {
+	for _, diag := range p.m.Diagnostics {
 		if diag.URI == uri {
 			diags = append(diags, diag)
 		}
 	}
-
-	if p.loadDiagnostics != nil {
-		res, err := p.loadDiagnostics.Get(ctx, s)
-		if err != nil {
-			return nil, err
-		}
-		for _, diag := range res.([]*source.Diagnostic) {
-			if diag.URI == uri {
-				diags = append(diags, diag)
-			}
+	for _, diag := range p.pkg.diagnostics {
+		if diag.URI == uri {
+			diags = append(diags, diag)
 		}
 	}
 
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 8463b3d..dd18018 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -669,7 +669,7 @@
 			}
 			continue
 		}
-		pkgs[i] = newPackage(ph.m, p)
+		pkgs[i] = &Package{ph.m, p}
 	}
 
 	return pkgs, firstErr
@@ -1144,6 +1144,20 @@
 	return match
 }
 
+// nearestModFile finds the nearest go.mod file contained in the directory
+// containing uri, or a parent of that directory.
+//
+// The given uri must be a file, not a directory.
+func nearestModFile(ctx context.Context, uri span.URI, fs source.FileSource) (span.URI, error) {
+	// TODO(rfindley)
+	dir := filepath.Dir(uri.Filename())
+	mod, err := findRootPattern(ctx, dir, "go.mod", fs)
+	if err != nil {
+		return "", err
+	}
+	return span.URIFromPath(mod), nil
+}
+
 func (s *snapshot) Metadata(id PackageID) *source.Metadata {
 	s.mu.Lock()
 	defer s.mu.Unlock()
@@ -1216,20 +1230,27 @@
 // GetFile succeeds even if the file does not exist. A non-nil error return
 // indicates some type of internal error, for example if ctx is cancelled.
 func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
-	s.view.markKnown(uri)
-
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	if fh, ok := s.files.Get(uri); ok {
+	return lockedSnapshot{s}.GetFile(ctx, uri)
+}
+
+// A lockedSnapshot implements the source.FileSource interface while holding
+// the lock for the wrapped snapshot.
+type lockedSnapshot struct{ wrapped *snapshot }
+
+func (s lockedSnapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
+	s.wrapped.view.markKnown(uri)
+	if fh, ok := s.wrapped.files.Get(uri); ok {
 		return fh, nil
 	}
 
-	fh, err := s.view.fs.GetFile(ctx, uri) // read the file
+	fh, err := s.wrapped.view.fs.GetFile(ctx, uri) // read the file
 	if err != nil {
 		return nil, err
 	}
-	s.files.Set(uri, fh)
+	s.wrapped.files.Set(uri, fh)
 	return fh, nil
 }
 
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index cc3ee5d..172a4fb 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -468,7 +468,8 @@
 	DepsByPkgPath   map[PackagePath]PackageID // values are unique and non-empty
 	Module          *packages.Module
 	DepsErrors      []*packagesinternal.PackageError
-	LoadDir         string // directory from which go/packages was run
+	Diagnostics     []*Diagnostic // processed diagnostics from 'go list'
+	LoadDir         string        // directory from which go/packages was run
 }
 
 func (m *Metadata) String() string { return string(m.ID) }