gopls/internal/lsp: eliminate more unnecessary typechecking

This change downgrades type-checking operations to mere
metadata queries or calls to the parser in these places:
- cmd toggle-gc-details
- cmd list-imports
- references
- rename (3 places)

The parsePackageNameDecl function (formerly isInPackageName)
only parses the package and imports declarations.

Also, eliminate the last two uses of Snapshot.WorkspacePackageByID
and the method itself.

Change-Id: I2bd24c24c9a2c14c9ba15e9624222b4aaf231c79
Reviewed-on: https://go-review.googlesource.com/c/tools/+/456040
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 8b02dea..52c5a26 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -1197,10 +1197,6 @@
 	return meta, nil
 }
 
-func (s *snapshot) WorkspacePackageByID(ctx context.Context, id PackageID) (source.Package, error) {
-	return s.checkedPackage(ctx, id, s.workspaceParseMode(id))
-}
-
 func (s *snapshot) CachedImportPaths(ctx context.Context) (map[PackagePath]source.Package, error) {
 	// Don't reload workspace package metadata.
 	// This function is meant to only return currently cached information.
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index 747adc8..4bbf0a9 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -691,17 +691,17 @@
 		progress:    "Toggling GC Details",
 		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		// TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed.
-		pkg, err := deps.snapshot.PackageForFile(ctx, deps.fh.URI(), source.TypecheckWorkspace, source.NarrowestPackage)
+		metas, err := deps.snapshot.MetadataForFile(ctx, deps.fh.URI())
 		if err != nil {
 			return err
 		}
+		id := metas[0].ID // 0 => narrowest package
 		c.s.gcOptimizationDetailsMu.Lock()
-		if _, ok := c.s.gcOptimizationDetails[pkg.ID()]; ok {
-			delete(c.s.gcOptimizationDetails, pkg.ID())
+		if _, ok := c.s.gcOptimizationDetails[id]; ok {
+			delete(c.s.gcOptimizationDetails, id)
 			c.s.clearDiagnosticSource(gcDetailsSource)
 		} else {
-			c.s.gcOptimizationDetails[pkg.ID()] = struct{}{}
+			c.s.gcOptimizationDetails[id] = struct{}{}
 		}
 		c.s.gcOptimizationDetailsMu.Unlock()
 		c.s.diagnoseSnapshot(deps.snapshot, nil, false)
@@ -758,16 +758,15 @@
 	err := c.run(ctx, commandConfig{
 		forURI: args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		// TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed.
-		pkg, err := deps.snapshot.PackageForFile(ctx, args.URI.SpanURI(), source.TypecheckWorkspace, source.NarrowestPackage)
+		fh, err := deps.snapshot.GetFile(ctx, args.URI.SpanURI())
 		if err != nil {
 			return err
 		}
-		pgf, err := pkg.File(args.URI.SpanURI())
+		pgf, err := deps.snapshot.ParseGo(ctx, fh, source.ParseHeader)
 		if err != nil {
 			return err
 		}
-		for _, group := range astutil.Imports(pkg.FileSet(), pgf.File) {
+		for _, group := range astutil.Imports(deps.snapshot.FileSet(), pgf.File) {
 			for _, imp := range group {
 				if imp.Path == nil {
 					continue
@@ -782,10 +781,16 @@
 				})
 			}
 		}
-		for _, imp := range pkg.Imports() {
-			result.PackageImports = append(result.PackageImports, command.PackageImport{
-				Path: string(imp.PkgPath()), // This might be the vendored path under GOPATH vendoring, in which case it's a bug.
-			})
+		metas, err := deps.snapshot.MetadataForFile(ctx, args.URI.SpanURI())
+		if err != nil {
+			return err // e.g. cancelled
+		}
+		if len(metas) == 0 {
+			return fmt.Errorf("no package containing %v", args.URI.SpanURI())
+		}
+		for pkgPath := range metas[0].DepsByPkgPath { // 0 => narrowest package
+			result.PackageImports = append(result.PackageImports,
+				command.PackageImport{Path: string(pkgPath)})
 		}
 		sort.Slice(result.PackageImports, func(i, j int) bool {
 			return result.PackageImports[i].Path < result.PackageImports[j].Path
diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go
index bf3784b..49b8294 100644
--- a/gopls/internal/lsp/source/references.go
+++ b/gopls/internal/lsp/source/references.go
@@ -9,12 +9,10 @@
 	"errors"
 	"fmt"
 	"go/ast"
-	"strings"
-
 	"go/token"
 	"go/types"
 	"sort"
-	"strconv"
+	"strings"
 
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/span"
@@ -32,36 +30,17 @@
 	isDeclaration bool
 }
 
-// isInPackageName reports whether the file's package name surrounds the
-// given position pp (e.g. "foo" surrounds the cursor in "package foo").
-func isInPackageName(ctx context.Context, s Snapshot, f FileHandle, pgf *ParsedGoFile, pp protocol.Position) (bool, error) {
-	// Find position of the package name declaration
-	cursorPos, err := pgf.Mapper.Pos(pp)
-	if err != nil {
-		return false, err
-	}
-
-	return pgf.File.Name.Pos() <= cursorPos && cursorPos <= pgf.File.Name.End(), nil
-}
-
 // References returns a list of references for a given identifier within the packages
 // containing i.File. Declarations appear first in the result.
 func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) {
 	ctx, done := event.Start(ctx, "source.References")
 	defer done()
 
-	// Find position of the package name declaration
-	pgf, err := s.ParseGo(ctx, f, ParseFull)
+	// Is the cursor within the package name declaration?
+	pgf, inPackageName, err := parsePackageNameDecl(ctx, s, f, pp)
 	if err != nil {
 		return nil, err
 	}
-
-	packageName := pgf.File.Name.Name // from package decl
-	inPackageName, err := isInPackageName(ctx, s, f, pgf, pp)
-	if err != nil {
-		return nil, err
-	}
-
 	if inPackageName {
 		// TODO(rfindley): this is inaccurate, excluding test variants, and
 		// redundant with package renaming. Refactor to share logic.
@@ -79,13 +58,12 @@
 		for _, dep := range rdeps {
 			for _, f := range dep.CompiledGoFiles() {
 				for _, imp := range f.File.Imports {
-					// TODO(adonovan): using UnquoteImport() here would
-					// reveal that there's an ImportPath==PackagePath
+					// TODO(adonovan): there's an ImportPath==PackagePath
 					// comparison that doesn't account for vendoring.
 					// Use dep.Metadata().DepsByPkgPath instead.
-					if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == string(renamingPkg.PkgPath()) {
+					if string(UnquoteImportPath(imp)) == string(renamingPkg.PkgPath()) {
 						refs = append(refs, &ReferenceInfo{
-							Name:        packageName,
+							Name:        pgf.File.Name.Name,
 							MappedRange: NewMappedRange(f.Tok, f.Mapper, imp.Pos(), imp.End()),
 						})
 					}
@@ -96,7 +74,7 @@
 		// Find internal references to the package within the package itself
 		for _, f := range renamingPkg.CompiledGoFiles() {
 			refs = append(refs, &ReferenceInfo{
-				Name:        packageName,
+				Name:        pgf.File.Name.Name,
 				MappedRange: NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()),
 			})
 		}
@@ -132,6 +110,20 @@
 	return refs, nil
 }
 
+// parsePackageNameDecl is a convenience function that parses and
+// returns the package name declaration of file fh, and reports
+// whether the position ppos lies within it.
+func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, ppos protocol.Position) (*ParsedGoFile, bool, error) {
+	pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader)
+	if err != nil {
+		return nil, false, err
+	}
+	// Careful: because we used ParseHeader,
+	// Mapper.Pos(ppos) may be beyond EOF => (0, err).
+	pos, _ := pgf.Mapper.Pos(ppos)
+	return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil
+}
+
 // references is a helper function to avoid recomputing qualifiedObjsAtProtocolPos.
 // The first element of qos is considered to be the declaration;
 // if isDeclaration, the first result is an extra item for it.
diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go
index 51555b0..6855e20 100644
--- a/gopls/internal/lsp/source/rename.go
+++ b/gopls/internal/lsp/source/rename.go
@@ -53,15 +53,12 @@
 	// Find position of the package name declaration.
 	ctx, done := event.Start(ctx, "source.PrepareRename")
 	defer done()
-	pgf, err := snapshot.ParseGo(ctx, f, ParseFull)
-	if err != nil {
-		return nil, err, err
-	}
-	inPackageName, err := isInPackageName(ctx, snapshot, f, pgf, pp)
-	if err != nil {
-		return nil, err, err
-	}
 
+	// Is the cursor within the package name declaration?
+	pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp)
+	if err != nil {
+		return nil, err, err
+	}
 	if inPackageName {
 		fileRenameSupported := false
 		for _, op := range snapshot.View().Options().SupportedResourceOperations {
@@ -106,17 +103,16 @@
 			err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path)
 			return nil, err, err
 		}
-		// TODO(rfindley): we should not need the package here.
-		pkg, err := snapshot.WorkspacePackageByID(ctx, meta.ID)
+
+		// Return the location of the package declaration.
+		rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End())
 		if err != nil {
-			err = fmt.Errorf("error building package to rename: %v", err)
 			return nil, err, err
 		}
-		result, err := computePrepareRenameResp(snapshot, pkg, pgf.File.Name, string(pkg.Name()))
-		if err != nil {
-			return nil, nil, err
-		}
-		return result, nil, nil
+		return &PrepareItem{
+			Range: rng,
+			Text:  string(meta.Name),
+		}, nil, nil
 	}
 
 	qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp)
@@ -171,15 +167,11 @@
 	ctx, done := event.Start(ctx, "source.Rename")
 	defer done()
 
-	pgf, err := s.ParseGo(ctx, f, ParseFull)
+	// Is the cursor within the package name declaration?
+	_, inPackageName, err := parsePackageNameDecl(ctx, s, f, pp)
 	if err != nil {
 		return nil, false, err
 	}
-	inPackageName, err := isInPackageName(ctx, s, f, pgf, pp)
-	if err != nil {
-		return nil, false, err
-	}
-
 	if inPackageName {
 		if !isValidIdentifier(newName) {
 			return nil, true, fmt.Errorf("%q is not a valid identifier", newName)
@@ -336,23 +328,24 @@
 // package clause has already been updated, to prevent duplicate edits.
 //
 // Edits are written into the edits map.
-func renamePackageClause(ctx context.Context, m *Metadata, s Snapshot, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error {
-	pkg, err := s.WorkspacePackageByID(ctx, m.ID)
-	if err != nil {
-		return err
-	}
-
+func renamePackageClause(ctx context.Context, m *Metadata, snapshot Snapshot, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error {
 	// Rename internal references to the package in the renaming package.
-	for _, f := range pkg.CompiledGoFiles() {
-		if seen.add(f.URI, m.PkgPath) {
+	for _, uri := range m.CompiledGoFiles {
+		if seen.add(uri, m.PkgPath) {
 			continue
 		}
-
+		fh, err := snapshot.GetFile(ctx, uri)
+		if err != nil {
+			return err
+		}
+		f, err := snapshot.ParseGo(ctx, fh, ParseHeader)
+		if err != nil {
+			return err
+		}
 		if f.File.Name == nil {
-			continue
+			continue // no package declaration
 		}
-		pkgNameMappedRange := NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End())
-		rng, err := pkgNameMappedRange.Range()
+		rng, err := f.Mapper.PosRange(f.File.Name.Pos(), f.File.Name.End())
 		if err != nil {
 			return err
 		}
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index 1084c74..1168370 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -104,6 +104,7 @@
 
 	// ParseGo returns the parsed AST for the file.
 	// If the file is not available, returns nil and an error.
+	// Position information is added to FileSet().
 	ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error)
 
 	// DiagnosePackage returns basic diagnostics, including list, parse, and type errors
@@ -206,10 +207,6 @@
 	// AllValidMetadata returns all valid metadata loaded for the snapshot.
 	AllValidMetadata(ctx context.Context) ([]*Metadata, error)
 
-	// WorkspacePackageByID returns the workspace package with id, type checked
-	// in 'workspace' mode.
-	WorkspacePackageByID(ctx context.Context, id PackageID) (Package, error)
-
 	// Symbols returns all symbols in the snapshot.
 	Symbols(ctx context.Context) map[span.URI][]Symbol