internal/lsp: fix race condition in type-checking

There has been a race condition that occasionally appears in test runs
on TryBots. Multiple threads perform type-checking, so they may race on
setting the fields of the *goFiles. Add a mutex to synchronize this.

Change-Id: If52c9d792c6504fc89044964998b06de7dfbd19c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/183978
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index d0ee975..b56be5f 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -192,8 +192,8 @@
 	return pkg, nil
 }
 
-func (imp *importer) cachePackage(ctx context.Context, p *pkg, meta *metadata, mode source.ParseMode) {
-	for _, file := range p.files {
+func (imp *importer) cachePackage(ctx context.Context, pkg *pkg, meta *metadata, mode source.ParseMode) {
+	for _, file := range pkg.files {
 		f, err := imp.view.getFile(file.uri)
 		if err != nil {
 			imp.view.session.log.Errorf(ctx, "no file: %v", err)
@@ -204,35 +204,9 @@
 			imp.view.session.log.Errorf(ctx, "%v is not a Go file", file.uri)
 			continue
 		}
-		// Set the package even if we failed to parse the file.
-		if gof.pkgs == nil {
-			gof.pkgs = make(map[packageID]*pkg)
+		if err := imp.cachePerFile(gof, file, pkg); err != nil {
+			imp.view.session.log.Errorf(ctx, "failed to cache file %s: %v", gof.URI(), err)
 		}
-		gof.pkgs[p.id] = p
-
-		// Get the AST for the file.
-		gof.ast = file
-		if gof.ast == nil {
-			imp.view.session.log.Errorf(ctx, "no AST information for %s", file.uri)
-			continue
-		}
-		if gof.ast.file == nil {
-			imp.view.session.log.Errorf(ctx, "no AST for %s: %v", file.uri, err)
-			continue
-		}
-		// Get the *token.File directly from the AST.
-		pos := gof.ast.file.Pos()
-		if !pos.IsValid() {
-			imp.view.session.log.Errorf(ctx, "AST for %s has an invalid position", file.uri)
-			continue
-		}
-		tok := imp.view.session.cache.FileSet().File(pos)
-		if tok == nil {
-			imp.view.session.log.Errorf(ctx, "no *token.File for %s", file.uri)
-			continue
-		}
-		gof.token = tok
-		gof.imports = gof.ast.file.Imports
 	}
 
 	// Set imports of package to correspond to cached packages.
@@ -243,10 +217,42 @@
 		if err != nil {
 			continue
 		}
-		p.imports[importPkg.pkgPath] = importPkg
+		pkg.imports[importPkg.pkgPath] = importPkg
 	}
 }
 
+func (imp *importer) cachePerFile(gof *goFile, file *astFile, p *pkg) error {
+	gof.mu.Lock()
+	defer gof.mu.Unlock()
+
+	// Set the package even if we failed to parse the file.
+	if gof.pkgs == nil {
+		gof.pkgs = make(map[packageID]*pkg)
+	}
+	gof.pkgs[p.id] = p
+
+	// Get the AST for the file.
+	gof.ast = file
+	if gof.ast == nil {
+		return fmt.Errorf("no AST information for %s", file.uri)
+	}
+	if gof.ast.file == nil {
+		return fmt.Errorf("no AST for %s", file.uri)
+	}
+	// Get the *token.File directly from the AST.
+	pos := gof.ast.file.Pos()
+	if !pos.IsValid() {
+		return fmt.Errorf("AST for %s has an invalid position", file.uri)
+	}
+	tok := imp.view.session.cache.FileSet().File(pos)
+	if tok == nil {
+		return fmt.Errorf("no *token.File for %s", file.uri)
+	}
+	gof.token = tok
+	gof.imports = gof.ast.file.Imports
+	return nil
+}
+
 func (c *cache) appendPkgError(pkg *pkg, err error) {
 	if err == nil {
 		return
diff --git a/internal/lsp/cache/gofile.go b/internal/lsp/cache/gofile.go
index 36ec12f..0351aa6 100644
--- a/internal/lsp/cache/gofile.go
+++ b/internal/lsp/cache/gofile.go
@@ -8,6 +8,7 @@
 	"context"
 	"go/ast"
 	"go/token"
+	"sync"
 
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
@@ -17,7 +18,9 @@
 type goFile struct {
 	fileBase
 
-	ast *astFile
+	// mu protects all mutable state of the Go file,
+	// which can be modified during type-checking.
+	mu sync.Mutex
 
 	// missingImports is the set of unresolved imports for this package.
 	// It contains any packages with `go list` errors.
@@ -28,9 +31,11 @@
 	// that we know about all of their packages.
 	justOpened bool
 
-	pkgs    map[packageID]*pkg
-	meta    map[packageID]*metadata
 	imports []*ast.ImportSpec
+
+	ast  *astFile
+	pkgs map[packageID]*pkg
+	meta map[packageID]*metadata
 }
 
 type astFile struct {
@@ -130,13 +135,16 @@
 }
 
 func unexpectedAST(ctx context.Context, f *goFile) bool {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
 	// If the AST comes back nil, something has gone wrong.
 	if f.ast == nil {
 		f.View().Session().Logger().Errorf(ctx, "expected full AST for %s, returned nil", f.URI())
 		return true
 	}
 	// If the AST comes back trimmed, something has gone wrong.
-	if f.astIsTrimmed() {
+	if f.ast.isTrimmed {
 		f.View().Session().Logger().Errorf(ctx, "expected full AST for %s, returned trimmed", f.URI())
 		return true
 	}
@@ -146,6 +154,9 @@
 // isDirty is true if the file needs to be type-checked.
 // It assumes that the file's view's mutex is held by the caller.
 func (f *goFile) isDirty() bool {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
 	// If the the file has just been opened,
 	// it may be part of more packages than we are aware of.
 	//
@@ -166,6 +177,9 @@
 }
 
 func (f *goFile) astIsTrimmed() bool {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
 	return f.ast != nil && f.ast.isTrimmed
 }
 
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 1368d56..913ef4a 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -20,59 +20,43 @@
 	// If the AST for this file is trimmed, and we are explicitly type-checking it,
 	// don't ignore function bodies.
 	if f.astIsTrimmed() {
+		v.pcache.mu.Lock()
 		f.invalidateAST()
+		v.pcache.mu.Unlock()
 	}
-	// Save the metadata's current missing imports, if any.
-	originalMissingImports := f.missingImports
 
-	// Check if we need to run go/packages.Load for this file's package.
-	if errs, err := v.checkMetadata(ctx, f); err != nil {
+	// Get the metadata for the file.
+	meta, errs, err := v.checkMetadata(ctx, f)
+	if err != nil {
 		return errs, err
 	}
-
-	// If `go list` failed to get data for the file in question (this should never happen).
-	if len(f.meta) == 0 {
-		return nil, fmt.Errorf("loadParseTypecheck: no metadata found for %v", f.filename())
-	}
-
-	// If we have already seen these missing imports before, and we still have type information,
-	// there is no need to continue.
-	if sameSet(originalMissingImports, f.missingImports) && len(f.pkgs) > 0 {
+	if meta == nil {
 		return nil, nil
 	}
-
-	for id, meta := range f.meta {
-		if _, ok := f.pkgs[id]; ok {
-			continue
-		}
+	for id, m := range meta {
 		imp := &importer{
 			view:          v,
 			seen:          make(map[packageID]struct{}),
 			ctx:           ctx,
-			fset:          f.FileSet(),
-			topLevelPkgID: meta.id,
+			fset:          v.session.cache.FileSet(),
+			topLevelPkgID: id,
 		}
 		// Start prefetching direct imports.
-		for importID := range meta.children {
+		for importID := range m.children {
 			go imp.getPkg(importID)
 		}
 		// Type-check package.
-		pkg, err := imp.getPkg(meta.id)
+		pkg, err := imp.getPkg(imp.topLevelPkgID)
 		if err != nil {
 			return nil, err
 		}
 		if pkg == nil || pkg.IsIllTyped() {
-			return nil, fmt.Errorf("loadParseTypecheck: %s is ill typed", meta.pkgPath)
-		}
-		// If we still have not found the package for the file, something is wrong.
-		if f.pkgs[id] == nil {
-			v.Session().Logger().Errorf(ctx, "failed to type-check package %s", meta.pkgPath)
+			return nil, fmt.Errorf("loadParseTypecheck: %s is ill typed", m.pkgPath)
 		}
 	}
 	if len(f.pkgs) == 0 {
-		return nil, fmt.Errorf("loadParseTypeCheck: no packages found for %v", f.filename())
+		return nil, fmt.Errorf("no packages found for %s", f.URI())
 	}
-
 	return nil, nil
 }
 
@@ -90,9 +74,15 @@
 
 // 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) ([]packages.Error, error) {
+func (v *view) checkMetadata(ctx context.Context, f *goFile) (map[packageID]*metadata, []packages.Error, error) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	// Save the metadata's current missing imports, if any.
+	originalMissingImports := f.missingImports
+
 	if !v.parseImports(ctx, f) {
-		return nil, nil
+		return f.meta, nil, nil
 	}
 
 	// Reset the file's metadata and type information if we are re-running `go list`.
@@ -109,7 +99,7 @@
 			err = fmt.Errorf("go/packages.Load: no packages found for %s", f.filename())
 		}
 		// Return this error as a diagnostic to the user.
-		return []packages.Error{
+		return nil, []packages.Error{
 			{
 				Msg:  err.Error(),
 				Kind: packages.UnknownError,
@@ -125,7 +115,7 @@
 		// If the package comes back with errors from `go list`,
 		// don't bother type-checking it.
 		if len(pkg.Errors) > 0 {
-			return pkg.Errors, fmt.Errorf("package %s has errors, skipping type-checking", pkg.PkgPath)
+			return nil, pkg.Errors, fmt.Errorf("package %s has errors, skipping type-checking", pkg.PkgPath)
 		}
 		for importPath, importPkg := range pkg.Imports {
 			if len(importPkg.Errors) > 0 {
@@ -138,7 +128,19 @@
 		// Build the import graph for this package.
 		v.link(ctx, packagePath(pkg.PkgPath), pkg, nil)
 	}
-	return nil, nil
+
+	// If `go list` failed to get data for the file in question (this should never happen).
+	if len(f.meta) == 0 {
+		return nil, nil, fmt.Errorf("loadParseTypecheck: no metadata found for %v", f.filename())
+	}
+
+	// If we have already seen these missing imports before, and we still have type information,
+	// there is no need to continue.
+	if sameSet(originalMissingImports, f.missingImports) && len(f.pkgs) != 0 {
+		return nil, nil, nil
+	}
+
+	return f.meta, nil, nil
 }
 
 // reparseImports reparses a file's package and import declarations to
@@ -158,6 +160,7 @@
 	if len(f.imports) != len(parsed.Imports) {
 		return true
 	}
+
 	for i, importSpec := range f.imports {
 		if importSpec.Path.Value != parsed.Imports[i].Path.Value {
 			return true
@@ -173,7 +176,9 @@
 	// If a file was added or deleted we need to invalidate the package cache
 	// so relevant packages get parsed and type-checked again.
 	if ok && !filenamesIdentical(m.files, pkg.CompiledGoFiles) {
-		v.invalidatePackage(id)
+		v.pcache.mu.Lock()
+		v.remove(id, make(map[packageID]struct{}))
+		v.pcache.mu.Unlock()
 	}
 
 	// If we haven't seen this package before.
@@ -234,18 +239,15 @@
 	if len(oldFiles) != len(newFiles) {
 		return false
 	}
-
 	oldByName := make(map[string]struct{}, len(oldFiles))
 	for _, filename := range oldFiles {
 		oldByName[filename] = struct{}{}
 	}
-
 	for _, newFilename := range newFiles {
 		if _, found := oldByName[newFilename]; !found {
 			return false
 		}
 		delete(oldByName, newFilename)
 	}
-
 	return len(oldByName) == 0
 }
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 9088053..16a928e 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -203,7 +203,9 @@
 				return
 			}
 			// Mark file as open.
+			gof.mu.Lock()
 			gof.justOpened = true
+			gof.mu.Unlock()
 		}
 	}
 }
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 76fade1..ec848a0 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -228,6 +228,12 @@
 	f.handleMu.Lock()
 	defer f.handleMu.Unlock()
 
+	f.view.mcache.mu.Lock()
+	defer f.view.mcache.mu.Unlock()
+
+	f.view.pcache.mu.Lock()
+	defer f.view.pcache.mu.Unlock()
+
 	f.invalidateAST()
 	f.handle = nil
 }
@@ -235,29 +241,20 @@
 // invalidateAST invalidates the AST of a Go file,
 // including any position and type information that depends on it.
 func (f *goFile) invalidateAST() {
-	f.view.pcache.mu.Lock()
-	defer f.view.pcache.mu.Unlock()
-
+	f.mu.Lock()
 	f.ast = nil
 	f.token = nil
+	pkgs := f.pkgs
+	f.mu.Unlock()
 
 	// Remove the package and all of its reverse dependencies from the cache.
-	for id, pkg := range f.pkgs {
+	for id, pkg := range pkgs {
 		if pkg != nil {
 			f.view.remove(id, map[packageID]struct{}{})
 		}
 	}
 }
 
-// invalidatePackage removes the specified package and dependents from the
-// package cache.
-func (v *view) invalidatePackage(id packageID) {
-	v.pcache.mu.Lock()
-	defer v.pcache.mu.Unlock()
-
-	v.remove(id, make(map[packageID]struct{}))
-}
-
 // remove invalidates a package and its reverse dependencies in the view's
 // package cache. It is assumed that the caller has locked both the mutexes
 // of both the mcache and the pcache.
@@ -278,7 +275,9 @@
 	for _, filename := range m.files {
 		if f, _ := v.findFile(span.FileURI(filename)); f != nil {
 			if gof, ok := f.(*goFile); ok {
+				gof.mu.Lock()
 				delete(gof.pkgs, id)
+				gof.mu.Unlock()
 			}
 		}
 	}
@@ -341,7 +340,11 @@
 			},
 		}
 		v.session.filesWatchMap.Watch(uri, func() {
-			f.(*goFile).invalidateContent()
+			gof, ok := f.(*goFile)
+			if !ok {
+				return
+			}
+			gof.invalidateContent()
 		})
 	}
 	v.mapFile(uri, f)