internal/lsp: fix race condition in metadata handling

The metadata was being added to the cache before it was fully computed.

Change-Id: I6931476a715f0383f7739fa4e950dcaa6cbec4fe
Reviewed-on: https://go-review.googlesource.com/c/tools/+/204562
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/load.go b/internal/lsp/cache/load.go
index 855f598..3408d3f 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -45,7 +45,6 @@
 	if err == context.Canceled {
 		return nil, errors.Errorf("no metadata for %s: %v", uri.Filename(), err)
 	}
-
 	log.Print(ctx, "go/packages.Load", tag.Of("packages", len(pkgs)))
 	if len(pkgs) == 0 {
 		if err == nil {
@@ -149,7 +148,7 @@
 		log.Print(ctx, "go/packages.Load", tag.Of("package", pkg.PkgPath), tag.Of("files", pkg.CompiledGoFiles))
 
 		// Set the metadata for this package.
-		if err := s.updateImports(ctx, packagePath(pkg.PkgPath), pkg, cfg); err != nil {
+		if err := s.updateImports(ctx, packagePath(pkg.PkgPath), pkg, cfg, map[packageID]struct{}{}); err != nil {
 			return nil, nil, err
 		}
 		m := s.getMetadata(packageID(pkg.ID))
@@ -167,16 +166,21 @@
 	return results, prevMissingImports, nil
 }
 
-func (s *snapshot) updateImports(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config) error {
+func (s *snapshot) updateImports(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) error {
+	id := packageID(pkg.ID)
+	if _, ok := seen[id]; ok {
+		return errors.Errorf("import cycle detected: %q", id)
+	}
 	// Recreate the metadata rather than reusing it to avoid locking.
 	m := &metadata{
-		id:         packageID(pkg.ID),
+		id:         id,
 		pkgPath:    pkgPath,
 		name:       pkg.Name,
 		typesSizes: pkg.TypesSizes,
 		errors:     pkg.Errors,
 		config:     cfg,
 	}
+	seen[id] = struct{}{}
 	for _, filename := range pkg.CompiledGoFiles {
 		uri := span.FileURI(filename)
 		m.files = append(m.files, uri)
@@ -184,16 +188,14 @@
 		s.addID(uri, m.id)
 	}
 
-	// Add the metadata to the cache.
-	s.setMetadata(m)
-
+	copied := make(map[packageID]struct{})
+	for k, v := range seen {
+		copied[k] = v
+	}
 	for importPath, importPkg := range pkg.Imports {
 		importPkgPath := packagePath(importPath)
 		importID := packageID(importPkg.ID)
 
-		if importPkgPath == pkgPath {
-			return errors.Errorf("cycle detected in %s", importPath)
-		}
 		m.deps = append(m.deps, importID)
 
 		// Don't remember any imports with significant errors.
@@ -206,10 +208,14 @@
 		}
 		dep := s.getMetadata(importID)
 		if dep == nil {
-			if err := s.updateImports(ctx, importPkgPath, importPkg, cfg); err != nil {
+			if err := s.updateImports(ctx, importPkgPath, importPkg, cfg, copied); err != nil {
 				log.Error(ctx, "error in dependency", err)
 			}
 		}
 	}
+
+	// Add the metadata to the cache.
+	s.setMetadata(m)
+
 	return nil
 }
diff --git a/internal/lsp/cmd/test/check.go b/internal/lsp/cmd/test/check.go
index 5fafa1f..ad260d2 100644
--- a/internal/lsp/cmd/test/check.go
+++ b/internal/lsp/cmd/test/check.go
@@ -61,7 +61,7 @@
 		}
 		_, found := got[expect]
 		if !found {
-			t.Errorf("missing diagnostic %q", expect)
+			t.Errorf("missing diagnostic %q, %v", expect, got)
 		} else {
 			delete(got, expect)
 		}
diff --git a/internal/lsp/testdata/nodisk/nodisk.overlay.go b/internal/lsp/testdata/nodisk/nodisk.overlay.go
index f9194be..0831f0e 100644
--- a/internal/lsp/testdata/nodisk/nodisk.overlay.go
+++ b/internal/lsp/testdata/nodisk/nodisk.overlay.go
@@ -6,4 +6,4 @@
 
 func _() {
 	foo.Foo() //@complete("F", Foo, IntFoo, StructFoo)
-}
+}
\ No newline at end of file