internal/lsp: do not close over the handle in the memoize function

Closing over the checkPackageHandle creates a cycle that forces the
checkPackageHandle not to be garbage collected until the value is
created. If a value is never created, the handle will not be collected.

Change-Id: I0f94557da917330ebe307a0e843b16ca7382e210
Reviewed-on: https://go-review.googlesource.com/c/tools/+/204079
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index 3af794f..70416ad 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -33,9 +33,6 @@
 	// mode is the mode the the files were parsed in.
 	mode source.ParseMode
 
-	// imports is the map of the package's imports.
-	imports map[packagePath]packageID
-
 	// m is the metadata associated with the package.
 	m *metadata
 
@@ -66,24 +63,33 @@
 	}
 
 	// Build the CheckPackageHandle for this ID and its dependencies.
-	cph, err := s.buildKey(ctx, id, mode)
+	cph, deps, err := s.buildKey(ctx, id, mode)
 	if err != nil {
 		return nil, err
 	}
-	fset := s.View().Session().Cache().FileSet()
-	h := s.view.session.cache.store.Bind(string(cph.key), func(ctx context.Context) interface{} {
+
+	// Do not close over the checkPackageHandle or the snapshot in the Bind function.
+	// This creates a cycle, which causes the finalizers to never run on the handles.
+	// The possible cycles are:
+	//
+	//     checkPackageHandle.h.function -> checkPackageHandle
+	//     checkPackageHandle.h.function -> snapshot -> checkPackageHandle
+	//
+
+	m := cph.m
+	files := cph.files
+	key := cph.key
+	fset := s.view.session.cache.fset
+
+	h := s.view.session.cache.store.Bind(string(key), func(ctx context.Context) interface{} {
 		// Begin loading the direct dependencies, in parallel.
-		for _, impID := range cph.imports {
-			dep := s.getPackage(impID, source.ParseExported)
-			if dep == nil {
-				continue
-			}
+		for _, dep := range deps {
 			go func(dep *checkPackageHandle) {
 				dep.check(ctx)
 			}(dep)
 		}
 		data := &checkPackageData{}
-		data.pkg, data.err = s.typeCheck(ctx, fset, cph)
+		data.pkg, data.err = typeCheck(ctx, fset, m, mode, files, deps)
 		return data
 	})
 	cph.handle = h
@@ -95,31 +101,35 @@
 }
 
 // buildKey computes the checkPackageKey for a given checkPackageHandle.
-func (s *snapshot) buildKey(ctx context.Context, id packageID, mode source.ParseMode) (*checkPackageHandle, error) {
+func (s *snapshot) buildKey(ctx context.Context, id packageID, mode source.ParseMode) (*checkPackageHandle, map[packagePath]*checkPackageHandle, error) {
 	m := s.getMetadata(id)
 	if m == nil {
-		return nil, errors.Errorf("no metadata for %s", id)
+		return nil, nil, errors.Errorf("no metadata for %s", id)
 	}
 	phs, err := s.parseGoHandles(ctx, m, mode)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	cph := &checkPackageHandle{
-		m:       m,
-		files:   phs,
-		imports: make(map[packagePath]packageID),
-		mode:    mode,
+		m:     m,
+		files: phs,
+		mode:  mode,
 	}
 
-	// Make sure all of the deps are sorted.
-	deps := append([]packageID{}, m.deps...)
-	sort.Slice(deps, func(i, j int) bool {
-		return deps[i] < deps[j]
+	// Make sure all of the depList are sorted.
+	var depList []packageID
+	for _, id := range m.deps {
+		depList = append(depList, id)
+	}
+	sort.Slice(depList, func(i, j int) bool {
+		return depList[i] < depList[j]
 	})
 
+	deps := make(map[packagePath]*checkPackageHandle)
+
 	// Begin computing the key by getting the depKeys for all dependencies.
 	var depKeys [][]byte
-	for _, depID := range deps {
+	for _, depID := range depList {
 		depHandle, err := s.checkPackageHandle(ctx, depID, source.ParseExported)
 		if err != nil {
 			log.Error(ctx, "no dep handle", err, telemetry.Package.Of(depID))
@@ -129,12 +139,11 @@
 			depKeys = append(depKeys, []byte(fmt.Sprintf("%s import not found", id)))
 			continue
 		}
-		cph.imports[depHandle.m.pkgPath] = depHandle.m.id
+		deps[depHandle.m.pkgPath] = depHandle
 		depKeys = append(depKeys, depHandle.key)
 	}
 	cph.key = checkPackageKey(cph.m.id, cph.files, m.config, depKeys)
-
-	return cph, nil
+	return cph, deps, nil
 }
 
 func checkPackageKey(id packageID, phs []source.ParseGoHandle, cfg *packages.Config, deps [][]byte) []byte {
@@ -213,22 +222,22 @@
 	return phs, nil
 }
 
-func (s *snapshot) typeCheck(ctx context.Context, fset *token.FileSet, cph *checkPackageHandle) (*pkg, error) {
-	ctx, done := trace.StartSpan(ctx, "cache.importer.typeCheck", telemetry.Package.Of(cph.m.id))
+func typeCheck(ctx context.Context, fset *token.FileSet, m *metadata, mode source.ParseMode, phs []source.ParseGoHandle, deps map[packagePath]*checkPackageHandle) (*pkg, error) {
+	ctx, done := trace.StartSpan(ctx, "cache.importer.typeCheck", telemetry.Package.Of(m.id))
 	defer done()
 
 	var rawErrors []error
-	for _, err := range cph.m.errors {
+	for _, err := range m.errors {
 		rawErrors = append(rawErrors, err)
 	}
 
 	pkg := &pkg{
-		id:         cph.m.id,
-		mode:       cph.mode,
-		pkgPath:    cph.m.pkgPath,
-		files:      cph.Files(),
+		id:         m.id,
+		pkgPath:    m.pkgPath,
+		mode:       mode,
+		files:      phs,
 		imports:    make(map[packagePath]*pkg),
-		typesSizes: cph.m.typesSizes,
+		typesSizes: m.typesSizes,
 		typesInfo: &types.Info{
 			Types:      make(map[ast.Expr]types.TypeAndValue),
 			Defs:       make(map[*ast.Ident]types.Object),
@@ -274,7 +283,7 @@
 	} else if len(files) == 0 { // not the unsafe package, no parsed files
 		return nil, errors.Errorf("no parsed files for package %s", pkg.pkgPath)
 	} else {
-		pkg.types = types.NewPackage(string(cph.m.pkgPath), cph.m.name)
+		pkg.types = types.NewPackage(string(m.pkgPath), m.name)
 	}
 
 	cfg := &types.Config{
@@ -282,13 +291,9 @@
 			rawErrors = append(rawErrors, e)
 		},
 		Importer: importerFunc(func(pkgPath string) (*types.Package, error) {
-			impID, ok := cph.imports[packagePath(pkgPath)]
-			if !ok {
-				return nil, errors.Errorf("no package data for import %s", pkgPath)
-			}
-			dep := s.getPackage(impID, source.ParseExported)
+			dep := deps[packagePath(pkgPath)]
 			if dep == nil {
-				return nil, errors.Errorf("no package for import %s", impID)
+				return nil, errors.Errorf("no package for import %s", pkgPath)
 			}
 			depPkg, err := dep.check(ctx)
 			if err != nil {
@@ -298,13 +303,13 @@
 			return depPkg.types, nil
 		}),
 	}
-	check := types.NewChecker(cfg, s.view.session.cache.FileSet(), pkg.types, pkg.typesInfo)
+	check := types.NewChecker(cfg, fset, pkg.types, pkg.typesInfo)
 
 	// Type checking errors are handled via the config, so ignore them here.
 	_ = check.Files(files)
 
 	// We don't care about a package's errors unless we have parsed it in full.
-	if cph.mode == source.ParseFull {
+	if mode == source.ParseFull {
 		for _, e := range rawErrors {
 			srcErr, err := sourceError(ctx, fset, pkg, e)
 			if err != nil {
diff --git a/internal/lsp/testdata/bad/badimport.go b/internal/lsp/testdata/bad/badimport.go
index 8960ce1..f7d8a11 100644
--- a/internal/lsp/testdata/bad/badimport.go
+++ b/internal/lsp/testdata/bad/badimport.go
@@ -1,5 +1,5 @@
 package bad
 
 import (
-	_ "nosuchpkg" //@diag("_", "compiler", "could not import nosuchpkg (no package data for import nosuchpkg)")
+	_ "nosuchpkg" //@diag("_", "compiler", "could not import nosuchpkg (no package for import nosuchpkg)")
 )