internal/lsp/cache: fix race condition in type-checking

We had previously been returning the metadata map in a few of the
loading functions. We don't actually need the map; we only need the
actual metadata. The race was caused by the return of the f.meta field
in a few functions, unprotected by the f.mu lock. This was likely a
result of the f.mu lock being added after the fact.

Fixes golang/go#33978

Change-Id: Ice5778d9d6dea23304237baf321b55d4fee6599c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/193717
Reviewed-by: Ian Cottrell <iancottrell@google.com>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/internal/lsp/cache/gofile.go b/internal/lsp/cache/gofile.go
index e34ec83..083d459 100644
--- a/internal/lsp/cache/gofile.go
+++ b/internal/lsp/cache/gofile.go
@@ -40,6 +40,15 @@
 	meta map[packageID]*metadata
 }
 
+// metadata assumes that the caller holds the f.mu lock.
+func (f *goFile) metadata() []*metadata {
+	result := make([]*metadata, 0, len(f.meta))
+	for _, m := range f.meta {
+		result = append(result, m)
+	}
+	return result
+}
+
 func (f *goFile) GetToken(ctx context.Context) (*token.File, error) {
 	file, err := f.GetAST(ctx, source.ParseFull)
 	if file == nil {
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index df4f938..9c4b242 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -46,7 +46,7 @@
 	return nil
 }
 
-func (view *view) load(ctx context.Context, f *goFile) (map[packageID]*metadata, error) {
+func (view *view) load(ctx context.Context, f *goFile) ([]*metadata, error) {
 	view.mu.Lock()
 	defer view.mu.Unlock()
 
@@ -84,9 +84,16 @@
 
 // checkMetadata determines if we should run go/packages.Load for this file.
 // If yes, update the metadata for the file and its package.
-func (v *view) checkMetadata(ctx context.Context, f *goFile) (map[packageID]*metadata, error) {
-	if !v.shouldRunGopackages(ctx, f) {
-		return f.meta, nil
+func (v *view) checkMetadata(ctx context.Context, f *goFile) ([]*metadata, error) {
+	// Check if we need to re-run go/packages before loading the package.
+	f.mu.Lock()
+	runGopackages := v.shouldRunGopackages(ctx, f)
+	metadata := f.metadata()
+	f.mu.Unlock()
+
+	// The package metadata is correct as-is, so just return it.
+	if !runGopackages {
+		return metadata, nil
 	}
 
 	// Check if the context has been canceled before calling packages.Load.
@@ -127,7 +134,7 @@
 	return m, nil
 }
 
-func validateMetadata(ctx context.Context, missingImports map[packagePath]struct{}, f *goFile) (map[packageID]*metadata, error) {
+func validateMetadata(ctx context.Context, missingImports map[packagePath]struct{}, f *goFile) ([]*metadata, error) {
 	f.mu.Lock()
 	defer f.mu.Unlock()
 
@@ -141,9 +148,11 @@
 	if sameSet(missingImports, f.missingImports) && len(f.pkgs) != 0 {
 		return nil, nil
 	}
+
 	// Otherwise, update the missing imports map.
 	f.missingImports = missingImports
-	return f.meta, nil
+
+	return f.metadata(), nil
 }
 
 func sameSet(x, y map[packagePath]struct{}) bool {
@@ -158,10 +167,10 @@
 	return true
 }
 
-// reparseImports reparses a file's package and import declarations to
+// shouldRunGopackages reparses a file's package and import declarations to
 // determine if they have changed.
+// It assumes that the caller holds the lock on the f.mu lock.
 func (v *view) shouldRunGopackages(ctx context.Context, f *goFile) (result bool) {
-	f.mu.Lock()
 	defer func() {
 		// Clear metadata if we are intending to re-run go/packages.
 		if result {
@@ -173,7 +182,6 @@
 				delete(f.pkgs, k)
 			}
 		}
-		f.mu.Unlock()
 	}()
 
 	if len(f.meta) == 0 || len(f.missingImports) > 0 {