internal/lsp: refactor various module-specific handles in cache

This change separates out different functions of mod handles.
Previously, we had ModHandle and ModTidyHandle. ModHandle was used to
parse go.mod files and get the results of `go mod why` and possible
dependency upgrades.

Now, we factor this out into 4 handles: ParseModHandle, ModWhyHandle,
ModUpgradeHandle, and ModTidyHandle. This allows each handle to be
specific to its own functionality. It also simplifies the code a bit,
as the handles can be written in terms of ParseModHandles instead of
FileHandles.

I may have some follow-up CLs to refactor the `go mod tidy` logic out of
the cache package, though I'm no longer certain that that's a good
choice.

Change-Id: I8e12299dfdda7bb61b05903d9aa474461d7f4836
Reviewed-on: https://go-review.googlesource.com/c/tools/+/239117
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index 09ce2ee..8a72479 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -59,7 +59,7 @@
 	err   error
 }
 
-func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
+func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) {
 	var modTime time.Time
 	if fi, err := os.Stat(uri.Filename()); err == nil {
 		modTime = fi.ModTime()
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index d8d618f..bcd04f5 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -6,12 +6,14 @@
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"path/filepath"
+	"regexp"
+	"strconv"
 	"strings"
 
 	"golang.org/x/mod/modfile"
-	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -22,231 +24,351 @@
 )
 
 const (
-	ModTidyError = "go mod tidy"
-	SyntaxError  = "syntax"
+	SyntaxError = "syntax"
 )
 
-type modKey struct {
-	sessionID string
-	cfg       string
-	gomod     string
-	view      string
-}
-
-type modTidyKey struct {
-	sessionID       string
-	cfg             string
-	gomod           string
-	imports         string
-	unsavedOverlays string
-	view            string
-}
-
-type modHandle struct {
+type parseModHandle struct {
 	handle *memoize.Handle
 
-	file source.FileHandle
-	cfg  *packages.Config
+	mod, sum source.FileHandle
 }
 
-type modData struct {
+type parseModData struct {
 	memoize.NoCopy
 
-	// parsed contains the parsed contents that are used to diff with
-	// the ideal contents.
 	parsed *modfile.File
+	m      *protocol.ColumnMapper
 
-	// m is the column mapper for the original go.mod file.
-	m *protocol.ColumnMapper
+	// parseErrors refers to syntax errors found in the go.mod file.
+	parseErrors []source.Error
 
-	// upgrades is a map of path->version that contains any upgrades for the go.mod.
-	upgrades map[string]string
-
-	// why is a map of path->explanation that contains all the "go mod why" contents
-	// for each require statement.
-	why map[string]string
-
-	// err is any error that occurs while we are calculating the parseErrors.
+	// err is any error encountered while parsing the file.
 	err error
 }
 
-func (mh *modHandle) String() string {
-	return mh.File().URI().Filename()
+func (mh *parseModHandle) Mod() source.FileHandle {
+	return mh.mod
 }
 
-func (mh *modHandle) File() source.FileHandle {
-	return mh.file
+func (mh *parseModHandle) Sum() source.FileHandle {
+	return mh.sum
 }
 
-func (mh *modHandle) Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error) {
+func (mh *parseModHandle) Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, []source.Error, error) {
 	v := mh.handle.Get(ctx)
 	if v == nil {
-		return nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI())
+		return nil, nil, nil, ctx.Err()
 	}
-	data := v.(*modData)
-	return data.parsed, data.m, data.err
+	data := v.(*parseModData)
+	return data.parsed, data.m, data.parseErrors, data.err
 }
 
-func (mh *modHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
-	v := mh.handle.Get(ctx)
-	if v == nil {
-		return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI())
-	}
-	data := v.(*modData)
-	return data.parsed, data.m, data.upgrades, data.err
-}
-
-func (mh *modHandle) Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
-	v := mh.handle.Get(ctx)
-	if v == nil {
-		return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI())
-	}
-	data := v.(*modData)
-	return data.parsed, data.m, data.why, data.err
-}
-
-func (s *snapshot) ModHandle(ctx context.Context, modFH source.FileHandle) (source.ModHandle, error) {
-	if err := s.awaitLoaded(ctx); err != nil {
-		return nil, err
-	}
-	var sumFH source.FileHandle
-	if s.view.sumURI != "" {
-		var err error
-		sumFH, err = s.GetFile(ctx, s.view.sumURI)
-		if err != nil {
-			return nil, err
-		}
-	}
-	var (
-		cfg    = s.config(ctx)
-		modURI = s.view.modURI
-		tmpMod = s.view.tmpMod
-	)
+func (s *snapshot) ParseModHandle(ctx context.Context, modFH source.FileHandle) (source.ParseModHandle, error) {
 	if handle := s.getModHandle(modFH.URI()); handle != nil {
 		return handle, nil
 	}
-	key := modKey{
-		sessionID: s.view.session.id,
-		cfg:       hashConfig(cfg),
-		gomod:     modFH.Identity().String(),
-		view:      s.view.folder.Filename(),
-	}
-	h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
-		ctx, done := event.Start(ctx, "cache.ModHandle", tag.URI.Of(modFH.URI()))
+	h := s.view.session.cache.store.Bind(modFH.Identity().String(), func(ctx context.Context) interface{} {
+		_, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI()))
 		defer done()
 
 		contents, err := modFH.Read()
 		if err != nil {
-			return &modData{
-				err: err,
-			}
+			return &parseModData{err: err}
 		}
-		parsedFile, err := modfile.Parse(modFH.URI().Filename(), contents, nil)
+		m := &protocol.ColumnMapper{
+			URI:       modFH.URI(),
+			Converter: span.NewContentConverter(modFH.URI().Filename(), contents),
+			Content:   contents,
+		}
+		parsed, err := modfile.Parse(modFH.URI().Filename(), contents, nil)
 		if err != nil {
-			return &modData{
-				err: err,
+			parseErr, _ := extractModParseErrors(modFH.URI(), m, err, contents)
+			var parseErrors []source.Error
+			if parseErr != nil {
+				parseErrors = append(parseErrors, *parseErr)
+			}
+			return &parseModData{
+				parseErrors: parseErrors,
+				err:         err,
 			}
 		}
-		data := &modData{
-			parsed: parsedFile,
-			m: &protocol.ColumnMapper{
-				URI:       modFH.URI(),
-				Converter: span.NewContentConverter(modFH.URI().Filename(), contents),
-				Content:   contents,
-			},
+		return &parseModData{
+			parsed: parsed,
+			m:      m,
 		}
+	})
+	// Get the go.sum file, either from the snapshot or directly from the
+	// cache. Avoid (*snapshot).GetFile here, as we don't want to add
+	// nonexistent file handles to the snapshot if the file does not exist.
+	sumURI := span.URIFromPath(sumFilename(modFH.URI()))
+	sumFH := s.FindFile(sumURI)
+	if sumFH == nil {
+		fh, err := s.view.session.cache.getFile(ctx, sumURI)
+		if err != nil && !os.IsNotExist(err) {
+			return nil, err
+		}
+		if fh.err != nil && !os.IsNotExist(fh.err) {
+			return nil, fh.err
+		}
+		// If the file doesn't exist, we can just keep the go.sum nil.
+		if err != nil || fh.err != nil {
+			sumFH = nil
+		} else {
+			sumFH = fh
+		}
+	}
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.parseModHandles[modFH.URI()] = &parseModHandle{
+		handle: h,
+		mod:    modFH,
+		sum:    sumFH,
+	}
+	return s.parseModHandles[modFH.URI()], nil
+}
 
-		// If this go.mod file is not the view's go.mod file, or if the
-		// -modfile flag is not supported, then we just want to parse.
-		if modFH.URI() != modURI {
-			return data
-		}
+func sumFilename(modURI span.URI) string {
+	return modURI.Filename()[:len(modURI.Filename())-len("mod")] + "sum"
+}
 
-		// Only get dependency upgrades if the go.mod file is the same as the view's.
-		if err := dependencyUpgrades(ctx, cfg, modFH, sumFH, tmpMod, data); err != nil {
-			return &modData{err: err}
+// extractModParseErrors processes the raw errors returned by modfile.Parse,
+// extracting the filenames and line numbers that correspond to the errors.
+func extractModParseErrors(uri span.URI, m *protocol.ColumnMapper, parseErr error, content []byte) (*source.Error, error) {
+	re := regexp.MustCompile(`.*:([\d]+): (.+)`)
+	matches := re.FindStringSubmatch(strings.TrimSpace(parseErr.Error()))
+	if len(matches) < 3 {
+		return nil, errors.Errorf("could not parse go.mod error message: %s", parseErr)
+	}
+	line, err := strconv.Atoi(matches[1])
+	if err != nil {
+		return nil, err
+	}
+	lines := strings.Split(string(content), "\n")
+	if line > len(lines) {
+		return nil, errors.Errorf("could not parse go.mod error message %q, line number %v out of range", content, line)
+	}
+	// The error returned from the modfile package only returns a line number,
+	// so we assume that the diagnostic should be for the entire line.
+	endOfLine := len(lines[line-1])
+	sOffset, err := m.Converter.ToOffset(line, 0)
+	if err != nil {
+		return nil, err
+	}
+	eOffset, err := m.Converter.ToOffset(line, endOfLine)
+	if err != nil {
+		return nil, err
+	}
+	spn := span.New(uri, span.NewPoint(line, 0, sOffset), span.NewPoint(line, endOfLine, eOffset))
+	rng, err := m.Range(spn)
+	if err != nil {
+		return nil, err
+	}
+	return &source.Error{
+		Category: SyntaxError,
+		Message:  matches[2],
+		Range:    rng,
+		URI:      uri,
+	}, nil
+}
+
+// modKey is uniquely identifies cached data for `go mod why` or dependencies
+// to upgrade.
+type modKey struct {
+	sessionID, cfg, mod, view string
+	verb                      modAction
+}
+
+type modAction int
+
+const (
+	why modAction = iota
+	upgrade
+)
+
+type modWhyHandle struct {
+	handle *memoize.Handle
+
+	pmh source.ParseModHandle
+}
+
+type modWhyData struct {
+	// why keeps track of the `go mod why` results for each require statement
+	// in the go.mod file.
+	why map[string]string
+
+	err error
+}
+
+func (mwh *modWhyHandle) Why(ctx context.Context) (map[string]string, error) {
+	v := mwh.handle.Get(ctx)
+	if v == nil {
+		return nil, ctx.Err()
+	}
+	data := v.(*modWhyData)
+	return data.why, data.err
+}
+
+func (s *snapshot) ModWhyHandle(ctx context.Context) (source.ModWhyHandle, error) {
+	if err := s.awaitLoaded(ctx); err != nil {
+		return nil, err
+	}
+	fh, err := s.GetFile(ctx, s.view.modURI)
+	if err != nil {
+		return nil, err
+	}
+	pmh, err := s.ParseModHandle(ctx, fh)
+	if err != nil {
+		return nil, err
+	}
+	var (
+		cfg    = s.config(ctx)
+		tmpMod = s.view.tmpMod
+	)
+	key := modKey{
+		sessionID: s.view.session.id,
+		cfg:       hashConfig(cfg),
+		mod:       pmh.Mod().Identity().String(),
+		view:      s.view.folder.Filename(),
+		verb:      why,
+	}
+	h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
+		ctx, done := event.Start(ctx, "cache.ModHandle", tag.URI.Of(pmh.Mod().URI()))
+		defer done()
+
+		parsed, _, _, err := pmh.Parse(ctx)
+		if err != nil {
+			return &modWhyData{err: err}
 		}
-		// Only run "go mod why" if the go.mod file is the same as the view's.
-		if err := goModWhy(ctx, cfg, modFH, sumFH, tmpMod, data); err != nil {
-			return &modData{err: err}
+		// No requires to explain.
+		if len(parsed.Require) == 0 {
+			return &modWhyData{}
 		}
-		return data
+		// Run `go mod why` on all the dependencies.
+		args := []string{"why", "-m"}
+		for _, req := range parsed.Require {
+			args = append(args, req.Mod.Path)
+		}
+		_, stdout, err := runGoCommand(ctx, cfg, pmh, tmpMod, "mod", args)
+		if err != nil {
+			return &modWhyData{err: err}
+		}
+		whyList := strings.Split(stdout.String(), "\n\n")
+		if len(whyList) != len(parsed.Require) {
+			return &modWhyData{
+				err: fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(parsed.Require)),
+			}
+		}
+		why := make(map[string]string, len(parsed.Require))
+		for i, req := range parsed.Require {
+			why[req.Mod.Path] = whyList[i]
+		}
+		return &modWhyData{why: why}
 	})
 	s.mu.Lock()
 	defer s.mu.Unlock()
-	s.modHandles[modFH.URI()] = &modHandle{
+	s.modWhyHandle = &modWhyHandle{
 		handle: h,
-		file:   modFH,
-		cfg:    cfg,
+		pmh:    pmh,
 	}
-	return s.modHandles[modFH.URI()], nil
+	return s.modWhyHandle, nil
 }
 
-func goModWhy(ctx context.Context, cfg *packages.Config, modFH, sumFH source.FileHandle, tmpMod bool, data *modData) error {
-	if len(data.parsed.Require) == 0 {
-		return nil
-	}
-	// Run "go mod why" on all the dependencies.
-	args := []string{"why", "-m"}
-	for _, req := range data.parsed.Require {
-		args = append(args, req.Mod.Path)
-	}
-	// If the -modfile flag is disabled, don't pass in a go.mod URI.
-	if !tmpMod {
-		modFH = nil
-	}
-	_, stdout, err := runGoCommand(ctx, cfg, modFH, sumFH, "mod", args)
-	if err != nil {
-		return err
-	}
-	whyList := strings.Split(stdout.String(), "\n\n")
-	if len(whyList) != len(data.parsed.Require) {
-		return nil
-	}
-	data.why = make(map[string]string, len(data.parsed.Require))
-	for i, req := range data.parsed.Require {
-		data.why[req.Mod.Path] = whyList[i]
-	}
-	return nil
+type modUpgradeHandle struct {
+	handle *memoize.Handle
+
+	pmh source.ParseModHandle
 }
 
-func dependencyUpgrades(ctx context.Context, cfg *packages.Config, modFH, sumFH source.FileHandle, tmpMod bool, data *modData) error {
-	if len(data.parsed.Require) == 0 {
-		return nil
+type modUpgradeData struct {
+	// upgrades maps modules to their latest versions.
+	upgrades map[string]string
+
+	err error
+}
+
+func (muh *modUpgradeHandle) Upgrades(ctx context.Context) (map[string]string, error) {
+	v := muh.handle.Get(ctx)
+	if v == nil {
+		return nil, ctx.Err()
 	}
-	// Run "go list -mod readonly -u -m all" to be able to see which deps can be
-	// upgraded without modifying mod file.
-	args := []string{"-u", "-m", "all"}
-	if !tmpMod || containsVendor(modFH.URI()) {
-		// Use -mod=readonly if the module contains a vendor directory
-		// (see golang/go#38711).
-		args = append([]string{"-mod", "readonly"}, args...)
+	data := v.(*modUpgradeData)
+	return data.upgrades, data.err
+}
+
+func (s *snapshot) ModUpgradeHandle(ctx context.Context) (source.ModUpgradeHandle, error) {
+	if err := s.awaitLoaded(ctx); err != nil {
+		return nil, err
 	}
-	// If the -modfile flag is disabled, don't pass in a go.mod URI.
-	if !tmpMod {
-		modFH = nil
-	}
-	_, stdout, err := runGoCommand(ctx, cfg, modFH, sumFH, "list", args)
+	fh, err := s.GetFile(ctx, s.view.modURI)
 	if err != nil {
-		return err
+		return nil, err
 	}
-	upgradesList := strings.Split(stdout.String(), "\n")
-	if len(upgradesList) <= 1 {
-		return nil
+	pmh, err := s.ParseModHandle(ctx, fh)
+	if err != nil {
+		return nil, err
 	}
-	data.upgrades = make(map[string]string)
-	for _, upgrade := range upgradesList[1:] {
-		// Example: "github.com/x/tools v1.1.0 [v1.2.0]"
-		info := strings.Split(upgrade, " ")
-		if len(info) < 3 {
-			continue
+	var (
+		cfg    = s.config(ctx)
+		tmpMod = s.view.tmpMod
+	)
+	key := modKey{
+		sessionID: s.view.session.id,
+		cfg:       hashConfig(cfg),
+		mod:       pmh.Mod().Identity().String(),
+		view:      s.view.folder.Filename(),
+		verb:      upgrade,
+	}
+	h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
+		ctx, done := event.Start(ctx, "cache.ModUpgradeHandle", tag.URI.Of(pmh.Mod().URI()))
+		defer done()
+
+		parsed, _, _, err := pmh.Parse(ctx)
+		if err != nil {
+			return &modUpgradeData{err: err}
 		}
-		dep, version := info[0], info[2]
-		latest := version[1:]                    // remove the "["
-		latest = strings.TrimSuffix(latest, "]") // remove the "]"
-		data.upgrades[dep] = latest
+		// No requires to upgrade.
+		if len(parsed.Require) == 0 {
+			return &modUpgradeData{}
+		}
+		// Run "go list -mod readonly -u -m all" to be able to see which deps can be
+		// upgraded without modifying mod file.
+		args := []string{"-u", "-m", "all"}
+		if !tmpMod || containsVendor(pmh.Mod().URI()) {
+			// Use -mod=readonly if the module contains a vendor directory
+			// (see golang/go#38711).
+			args = append([]string{"-mod", "readonly"}, args...)
+		}
+		_, stdout, err := runGoCommand(ctx, cfg, pmh, tmpMod, "list", args)
+		if err != nil {
+			return &modUpgradeData{err: err}
+		}
+		upgradesList := strings.Split(stdout.String(), "\n")
+		if len(upgradesList) <= 1 {
+			return nil
+		}
+		upgrades := make(map[string]string)
+		for _, upgrade := range upgradesList[1:] {
+			// Example: "github.com/x/tools v1.1.0 [v1.2.0]"
+			info := strings.Split(upgrade, " ")
+			if len(info) < 3 {
+				continue
+			}
+			dep, version := info[0], info[2]
+			latest := version[1:]                    // remove the "["
+			latest = strings.TrimSuffix(latest, "]") // remove the "]"
+			upgrades[dep] = latest
+		}
+		return &modUpgradeData{
+			upgrades: upgrades,
+		}
+	})
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.modUpgradeHandle = &modUpgradeHandle{
+		handle: h,
+		pmh:    pmh,
 	}
-	return nil
+	return s.modUpgradeHandle, nil
 }
 
 // containsVendor reports whether the module has a vendor folder.
diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go
index b8175a9..485b16b 100644
--- a/internal/lsp/cache/mod_tidy.go
+++ b/internal/lsp/cache/mod_tidy.go
@@ -8,12 +8,8 @@
 	"context"
 	"fmt"
 	"io/ioutil"
-	"regexp"
-	"strconv"
-	"strings"
 
 	"golang.org/x/mod/modfile"
-	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -21,77 +17,60 @@
 	"golang.org/x/tools/internal/memoize"
 	"golang.org/x/tools/internal/packagesinternal"
 	"golang.org/x/tools/internal/span"
-	errors "golang.org/x/xerrors"
 )
 
+type modTidyKey struct {
+	sessionID       string
+	cfg             string
+	gomod           string
+	imports         string
+	unsavedOverlays string
+	view            string
+}
+
 type modTidyHandle struct {
 	handle *memoize.Handle
 
-	file source.FileHandle
-	cfg  *packages.Config
+	pmh source.ParseModHandle
 }
 
 type modTidyData struct {
 	memoize.NoCopy
 
-	// fh is the file handle for the original go.mod file.
-	fh source.FileHandle
-
-	// parsed contains the parsed contents that are used to diff with
-	// the ideal contents.
-	parsed *modfile.File
-
-	// m is the column mapper for the original go.mod file.
-	m *protocol.ColumnMapper
-
-	// parseErrors are the errors that arise when we diff between a user's go.mod
-	// and the "tidied" go.mod.
-	parseErrors []source.Error
-
-	// ideal contains the parsed contents for the go.mod file
-	// after it has been "tidied".
-	ideal *modfile.File
-
-	// unusedDeps is the map containing the dependencies that are left after
-	// removing the ones that are identical in the original and ideal go.mods.
-	unusedDeps map[string]*modfile.Require
-
-	// missingDeps is the map containing that are missing from the original
-	// go.mod, but present in the ideal go.mod.
+	// missingDeps contains dependencies that should be added to the view's
+	// go.mod file.
 	missingDeps map[string]*modfile.Require
 
-	// err is any error that occurs while we are calculating the parseErrors.
+	// diagnostics are any errors and associated suggested fixes for
+	// the go.mod file.
+	diagnostics []source.Error
+
 	err error
 }
 
-func (mh *modTidyHandle) String() string {
-	return mh.File().URI().Filename()
-}
-
-func (mh *modTidyHandle) File() source.FileHandle {
-	return mh.file
-}
-
-func (mh *modTidyHandle) Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []source.Error, error) {
-	v := mh.handle.Get(ctx)
+func (mth *modTidyHandle) Tidy(ctx context.Context) (map[string]*modfile.Require, []source.Error, error) {
+	v := mth.handle.Get(ctx)
 	if v == nil {
-		return nil, nil, nil, nil, errors.Errorf("no tidied file for %s", mh.File().URI())
+		return nil, nil, ctx.Err()
 	}
 	data := v.(*modTidyData)
-	return data.parsed, data.m, data.missingDeps, data.parseErrors, data.err
+	return data.missingDeps, data.diagnostics, data.err
 }
 
-func (s *snapshot) ModTidyHandle(ctx context.Context, modFH source.FileHandle) (source.ModTidyHandle, error) {
+func (s *snapshot) ModTidyHandle(ctx context.Context) (source.ModTidyHandle, error) {
+	if !s.view.tmpMod {
+		return nil, source.ErrTmpModfileUnsupported
+	}
 	if handle := s.getModTidyHandle(); handle != nil {
 		return handle, nil
 	}
-	var sumFH source.FileHandle
-	if s.view.sumURI != "" {
-		var err error
-		sumFH, err = s.GetFile(ctx, s.view.sumURI)
-		if err != nil {
-			return nil, err
-		}
+	fh, err := s.GetFile(ctx, s.view.modURI)
+	if err != nil {
+		return nil, err
+	}
+	pmh, err := s.ParseModHandle(ctx, fh)
+	if err != nil {
+		return nil, err
 	}
 	wsPackages, err := s.WorkspacePackages(ctx)
 	if ctx.Err() != nil {
@@ -110,48 +89,31 @@
 	s.mu.Unlock()
 
 	var (
-		options = s.View().Options()
 		folder  = s.View().Folder()
 		modURI  = s.view.modURI
-		tmpMod  = s.view.tmpMod
+		cfg     = s.config(ctx)
+		options = s.view.Options()
 	)
-
-	cfg := s.config(ctx)
 	key := modTidyKey{
 		sessionID:       s.view.session.id,
 		view:            folder.Filename(),
 		imports:         imports,
 		unsavedOverlays: overlayHash,
-		gomod:           modFH.Identity().String(),
+		gomod:           pmh.Mod().Identity().String(),
 		cfg:             hashConfig(cfg),
 	}
 	h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
 		ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(modURI))
 		defer done()
 
-		// Do nothing if the -modfile flag is disabled or if the given go.mod
-		// is outside of our view.
-		if modURI != modFH.URI() || !tmpMod {
-			return &modTidyData{}
-		}
-
-		contents, err := modFH.Read()
-		if err != nil {
-			return &modTidyData{err: err}
-		}
-		realMapper := &protocol.ColumnMapper{
-			URI:       modURI,
-			Converter: span.NewContentConverter(modURI.Filename(), contents),
-			Content:   contents,
-		}
-		origParsedFile, err := modfile.Parse(modURI.Filename(), contents, nil)
-		if err != nil {
-			if parseErr, err := extractModParseErrors(ctx, modURI, realMapper, err, contents); err == nil {
-				return &modTidyData{parseErrors: []source.Error{parseErr}}
+		original, m, parseErrors, err := pmh.Parse(ctx)
+		if err != nil || len(parseErrors) > 0 {
+			return &modTidyData{
+				diagnostics: parseErrors,
+				err:         err,
 			}
-			return &modTidyData{err: err}
 		}
-		tmpURI, inv, cleanup, err := goCommandInvocation(ctx, cfg, modFH, sumFH, "mod", []string{"tidy"})
+		tmpURI, inv, cleanup, err := goCommandInvocation(ctx, cfg, pmh, "mod", []string{"tidy"})
 		if err != nil {
 			return &modTidyData{err: err}
 		}
@@ -167,117 +129,73 @@
 		if err != nil {
 			return &modTidyData{err: err}
 		}
-		idealParsedFile, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
+		ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
 		if err != nil {
 			// We do not need to worry about the temporary file's parse errors
 			// since it has been "tidied".
 			return &modTidyData{err: err}
 		}
-
-		data := &modTidyData{
-			fh:          modFH,
-			parsed:      origParsedFile,
-			m:           realMapper,
-			ideal:       idealParsedFile,
-			unusedDeps:  make(map[string]*modfile.Require, len(origParsedFile.Require)),
-			missingDeps: make(map[string]*modfile.Require, len(idealParsedFile.Require)),
-		}
 		// Get the dependencies that are different between the original and
 		// ideal go.mod files.
-		for _, req := range origParsedFile.Require {
-			data.unusedDeps[req.Mod.Path] = req
+		unusedDeps := make(map[string]*modfile.Require, len(original.Require))
+		missingDeps := make(map[string]*modfile.Require, len(ideal.Require))
+		for _, req := range original.Require {
+			unusedDeps[req.Mod.Path] = req
 		}
-		for _, req := range idealParsedFile.Require {
-			origDep := data.unusedDeps[req.Mod.Path]
+		for _, req := range ideal.Require {
+			origDep := unusedDeps[req.Mod.Path]
 			if origDep != nil && origDep.Indirect == req.Indirect {
-				delete(data.unusedDeps, req.Mod.Path)
+				delete(unusedDeps, req.Mod.Path)
 			} else {
-				data.missingDeps[req.Mod.Path] = req
+				missingDeps[req.Mod.Path] = req
 			}
 		}
-		data.parseErrors, data.err = modRequireErrors(options, data)
-
-		for _, req := range data.missingDeps {
-			if data.unusedDeps[req.Mod.Path] != nil {
-				delete(data.missingDeps, req.Mod.Path)
+		diagnostics, err := modRequireErrors(pmh.Mod().URI(), original, m, missingDeps, unusedDeps, options)
+		if err != nil {
+			return &modTidyData{err: err}
+		}
+		for _, req := range missingDeps {
+			if unusedDeps[req.Mod.Path] != nil {
+				delete(missingDeps, req.Mod.Path)
 			}
 		}
-		return data
+		return &modTidyData{
+			missingDeps: missingDeps,
+			diagnostics: diagnostics,
+		}
 	})
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	s.modTidyHandle = &modTidyHandle{
 		handle: h,
-		file:   modFH,
-		cfg:    cfg,
+		pmh:    pmh,
 	}
 	return s.modTidyHandle, nil
 }
 
-// extractModParseErrors processes the raw errors returned by modfile.Parse,
-// extracting the filenames and line numbers that correspond to the errors.
-func extractModParseErrors(ctx context.Context, uri span.URI, m *protocol.ColumnMapper, parseErr error, content []byte) (source.Error, error) {
-	re := regexp.MustCompile(`.*:([\d]+): (.+)`)
-	matches := re.FindStringSubmatch(strings.TrimSpace(parseErr.Error()))
-	if len(matches) < 3 {
-		event.Error(ctx, "could not parse golang/x/mod error message", parseErr)
-		return source.Error{}, parseErr
-	}
-	line, err := strconv.Atoi(matches[1])
-	if err != nil {
-		return source.Error{}, parseErr
-	}
-	lines := strings.Split(string(content), "\n")
-	if len(lines) <= line {
-		return source.Error{}, errors.Errorf("could not parse goland/x/mod error message, line number out of range")
-	}
-	// The error returned from the modfile package only returns a line number,
-	// so we assume that the diagnostic should be for the entire line.
-	endOfLine := len(lines[line-1])
-	sOffset, err := m.Converter.ToOffset(line, 0)
-	if err != nil {
-		return source.Error{}, err
-	}
-	eOffset, err := m.Converter.ToOffset(line, endOfLine)
-	if err != nil {
-		return source.Error{}, err
-	}
-	spn := span.New(uri, span.NewPoint(line, 0, sOffset), span.NewPoint(line, endOfLine, eOffset))
-	rng, err := m.Range(spn)
-	if err != nil {
-		return source.Error{}, err
-	}
-	return source.Error{
-		Category: SyntaxError,
-		Message:  matches[2],
-		Range:    rng,
-		URI:      uri,
-	}, nil
-}
-
 // modRequireErrors extracts the errors that occur on the require directives.
 // It checks for directness issues and unused dependencies.
-func modRequireErrors(options source.Options, data *modTidyData) ([]source.Error, error) {
+func modRequireErrors(uri span.URI, parsed *modfile.File, m *protocol.ColumnMapper, missingDeps, unusedDeps map[string]*modfile.Require, options source.Options) ([]source.Error, error) {
 	var errors []source.Error
-	for dep, req := range data.unusedDeps {
+	for dep, req := range unusedDeps {
 		if req.Syntax == nil {
 			continue
 		}
 		// Handle dependencies that are incorrectly labeled indirect and vice versa.
-		if data.missingDeps[dep] != nil && req.Indirect != data.missingDeps[dep].Indirect {
-			directErr, err := modDirectnessErrors(options, data, req)
+		if missingDeps[dep] != nil && req.Indirect != missingDeps[dep].Indirect {
+			directErr, err := modDirectnessErrors(uri, parsed, m, req, options)
 			if err != nil {
 				return nil, err
 			}
 			errors = append(errors, directErr)
 		}
 		// Handle unused dependencies.
-		if data.missingDeps[dep] == nil {
-			rng, err := rangeFromPositions(data.fh.URI(), data.m, req.Syntax.Start, req.Syntax.End)
+		if missingDeps[dep] == nil {
+			rng, err := rangeFromPositions(uri, m, req.Syntax.Start, req.Syntax.End)
 			if err != nil {
 				return nil, err
 			}
-			edits, err := dropDependencyEdits(options, data, req)
+			edits, err := dropDependencyEdits(uri, parsed, m, req, options)
 			if err != nil {
 				return nil, err
 			}
@@ -285,10 +203,12 @@
 				Category: ModTidyError,
 				Message:  fmt.Sprintf("%s is not used in this module.", dep),
 				Range:    rng,
-				URI:      data.fh.URI(),
+				URI:      uri,
 				SuggestedFixes: []source.SuggestedFix{{
 					Title: fmt.Sprintf("Remove dependency: %s", dep),
-					Edits: map[span.URI][]protocol.TextEdit{data.fh.URI(): edits},
+					Edits: map[span.URI][]protocol.TextEdit{
+						uri: edits,
+					},
 				}},
 			})
 		}
@@ -296,9 +216,11 @@
 	return errors, nil
 }
 
+const ModTidyError = "go mod tidy"
+
 // modDirectnessErrors extracts errors when a dependency is labeled indirect when it should be direct and vice versa.
-func modDirectnessErrors(options source.Options, data *modTidyData, req *modfile.Require) (source.Error, error) {
-	rng, err := rangeFromPositions(data.fh.URI(), data.m, req.Syntax.Start, req.Syntax.End)
+func modDirectnessErrors(uri span.URI, parsed *modfile.File, m *protocol.ColumnMapper, req *modfile.Require, options source.Options) (source.Error, error) {
+	rng, err := rangeFromPositions(uri, m, req.Syntax.Start, req.Syntax.End)
 	if err != nil {
 		return source.Error{}, err
 	}
@@ -308,12 +230,12 @@
 			end := comments.Suffix[0].Start
 			end.LineRune += len(comments.Suffix[0].Token)
 			end.Byte += len([]byte(comments.Suffix[0].Token))
-			rng, err = rangeFromPositions(data.fh.URI(), data.m, comments.Suffix[0].Start, end)
+			rng, err = rangeFromPositions(uri, m, comments.Suffix[0].Start, end)
 			if err != nil {
 				return source.Error{}, err
 			}
 		}
-		edits, err := changeDirectnessEdits(options, data, req, false)
+		edits, err := changeDirectnessEdits(uri, parsed, m, req, false, options)
 		if err != nil {
 			return source.Error{}, err
 		}
@@ -321,15 +243,17 @@
 			Category: ModTidyError,
 			Message:  fmt.Sprintf("%s should be a direct dependency.", req.Mod.Path),
 			Range:    rng,
-			URI:      data.fh.URI(),
+			URI:      uri,
 			SuggestedFixes: []source.SuggestedFix{{
 				Title: fmt.Sprintf("Make %s direct", req.Mod.Path),
-				Edits: map[span.URI][]protocol.TextEdit{data.fh.URI(): edits},
+				Edits: map[span.URI][]protocol.TextEdit{
+					uri: edits,
+				},
 			}},
 		}, nil
 	}
 	// If the dependency should be indirect, add the // indirect.
-	edits, err := changeDirectnessEdits(options, data, req, true)
+	edits, err := changeDirectnessEdits(uri, parsed, m, req, true, options)
 	if err != nil {
 		return source.Error{}, err
 	}
@@ -337,10 +261,12 @@
 		Category: ModTidyError,
 		Message:  fmt.Sprintf("%s should be an indirect dependency.", req.Mod.Path),
 		Range:    rng,
-		URI:      data.fh.URI(),
+		URI:      uri,
 		SuggestedFixes: []source.SuggestedFix{{
 			Title: fmt.Sprintf("Make %s indirect", req.Mod.Path),
-			Edits: map[span.URI][]protocol.TextEdit{data.fh.URI(): edits},
+			Edits: map[span.URI][]protocol.TextEdit{
+				uri: edits,
+			},
 		}},
 	}, nil
 }
@@ -357,20 +283,20 @@
 // 	module t
 //
 // 	go 1.11
-func dropDependencyEdits(options source.Options, data *modTidyData, req *modfile.Require) ([]protocol.TextEdit, error) {
-	if err := data.parsed.DropRequire(req.Mod.Path); err != nil {
+func dropDependencyEdits(uri span.URI, parsed *modfile.File, m *protocol.ColumnMapper, req *modfile.Require, options source.Options) ([]protocol.TextEdit, error) {
+	if err := parsed.DropRequire(req.Mod.Path); err != nil {
 		return nil, err
 	}
-	data.parsed.Cleanup()
-	newContents, err := data.parsed.Format()
+	parsed.Cleanup()
+	newContents, err := parsed.Format()
 	if err != nil {
 		return nil, err
 	}
 	// Reset the *modfile.File back to before we dropped the dependency.
-	data.parsed.AddNewRequire(req.Mod.Path, req.Mod.Version, req.Indirect)
+	parsed.AddNewRequire(req.Mod.Path, req.Mod.Version, req.Indirect)
 	// Calculate the edits to be made due to the change.
-	diff := options.ComputeEdits(data.fh.URI(), string(data.m.Content), string(newContents))
-	edits, err := source.ToProtocolEdits(data.m, diff)
+	diff := options.ComputeEdits(uri, string(m.Content), string(newContents))
+	edits, err := source.ToProtocolEdits(m, diff)
 	if err != nil {
 		return nil, err
 	}
@@ -391,34 +317,34 @@
 // 	go 1.11
 //
 // 	require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee // indirect
-func changeDirectnessEdits(options source.Options, data *modTidyData, req *modfile.Require, indirect bool) ([]protocol.TextEdit, error) {
+func changeDirectnessEdits(uri span.URI, parsed *modfile.File, m *protocol.ColumnMapper, req *modfile.Require, indirect bool, options source.Options) ([]protocol.TextEdit, error) {
 	var newReq []*modfile.Require
 	prevIndirect := false
 	// Change the directness in the matching require statement.
-	for _, r := range data.parsed.Require {
+	for _, r := range parsed.Require {
 		if req.Mod.Path == r.Mod.Path {
 			prevIndirect = req.Indirect
 			req.Indirect = indirect
 		}
 		newReq = append(newReq, r)
 	}
-	data.parsed.SetRequire(newReq)
-	data.parsed.Cleanup()
-	newContents, err := data.parsed.Format()
+	parsed.SetRequire(newReq)
+	parsed.Cleanup()
+	newContents, err := parsed.Format()
 	if err != nil {
 		return nil, err
 	}
 	// Change the dependency back to the way it was before we got the newContents.
-	for _, r := range data.parsed.Require {
+	for _, r := range parsed.Require {
 		if req.Mod.Path == r.Mod.Path {
 			req.Indirect = prevIndirect
 		}
 		newReq = append(newReq, r)
 	}
-	data.parsed.SetRequire(newReq)
+	parsed.SetRequire(newReq)
 	// Calculate the edits to be made due to the change.
-	diff := options.ComputeEdits(data.fh.URI(), string(data.m.Content), string(newContents))
-	edits, err := source.ToProtocolEdits(data.m, diff)
+	diff := options.ComputeEdits(uri, string(m.Content), string(newContents))
+	edits, err := source.ToProtocolEdits(m, diff)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 44636bc..527f54d 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -144,7 +144,7 @@
 			actions:           make(map[actionKey]*actionHandle),
 			workspacePackages: make(map[packageID]packagePath),
 			unloadableFiles:   make(map[span.URI]struct{}),
-			modHandles:        make(map[span.URI]*modHandle),
+			parseModHandles:   make(map[span.URI]*parseModHandle),
 		},
 		gocmdRunner: &gocommand.Runner{},
 	}
@@ -335,7 +335,7 @@
 			if o, ok := overlays[c.URI]; ok {
 				views[view][c.URI] = o
 			} else {
-				fh, err := s.cache.GetFile(ctx, c.URI)
+				fh, err := s.cache.getFile(ctx, c.URI)
 				if err != nil {
 					return nil, err
 				}
@@ -400,7 +400,7 @@
 		var sameContentOnDisk bool
 		switch c.Action {
 		case source.Open:
-			fh, err := s.cache.GetFile(ctx, c.URI)
+			fh, err := s.cache.getFile(ctx, c.URI)
 			if err != nil {
 				return nil, err
 			}
@@ -443,7 +443,7 @@
 		return overlay, nil
 	}
 	// Fall back to the cache-level file system.
-	return s.cache.GetFile(ctx, uri)
+	return s.cache.getFile(ctx, uri)
 }
 
 func (s *Session) readOverlay(uri span.URI) *overlay {
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 27d9612..3faa7e1 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -66,12 +66,18 @@
 	// unloadableFiles keeps track of files that we've failed to load.
 	unloadableFiles map[span.URI]struct{}
 
-	// modHandles keeps track of any ParseModHandles for this snapshot.
-	modHandles map[span.URI]*modHandle
+	// parseModHandles keeps track of any ParseModHandles for the snapshot.
+	// The handles need not refer to only the view's go.mod file.
+	parseModHandles map[span.URI]*parseModHandle
 
-	// modTidyHandle is the saved modTidyHandle for this snapshot, it is attached to the
-	// snapshot so we can reuse it without having to call "go mod tidy" everytime.
-	modTidyHandle *modTidyHandle
+	// Preserve go.mod-related handles to avoid garbage-collecting the results
+	// of various calls to the go command.
+	//
+	// TODO(rstambler): If we end up with any more such handles, we should
+	// consider creating a struct for them.
+	modTidyHandle    *modTidyHandle
+	modWhyHandle     *modWhyHandle
+	modUpgradeHandle *modUpgradeHandle
 }
 
 type packageKey struct {
@@ -135,47 +141,41 @@
 
 func (s *snapshot) RunGoCommandDirect(ctx context.Context, verb string, args []string) error {
 	cfg := s.config(ctx)
-	_, _, err := runGoCommand(ctx, cfg, nil, nil, verb, args)
+	_, _, err := runGoCommand(ctx, cfg, nil, s.view.tmpMod, verb, args)
 	return err
 }
 
 func (s *snapshot) RunGoCommand(ctx context.Context, verb string, args []string) (*bytes.Buffer, error) {
 	cfg := s.config(ctx)
-	var modFH, sumFH source.FileHandle
+	var pmh source.ParseModHandle
 	if s.view.tmpMod {
-		var err error
-		modFH, err = s.GetFile(ctx, s.view.modURI)
+		modFH, err := s.GetFile(ctx, s.view.modURI)
 		if err != nil {
 			return nil, err
 		}
-		if s.view.sumURI != "" {
-			sumFH, err = s.GetFile(ctx, s.view.sumURI)
-			if err != nil {
-				return nil, err
-			}
+		pmh, err = s.ParseModHandle(ctx, modFH)
+		if err != nil {
+			return nil, err
 		}
 	}
-	_, stdout, err := runGoCommand(ctx, cfg, modFH, sumFH, verb, args)
+	_, stdout, err := runGoCommand(ctx, cfg, pmh, s.view.tmpMod, verb, args)
 	return stdout, err
 }
 
 func (s *snapshot) RunGoCommandPiped(ctx context.Context, verb string, args []string, stdout, stderr io.Writer) error {
 	cfg := s.config(ctx)
-	var modFH, sumFH source.FileHandle
+	var pmh source.ParseModHandle
 	if s.view.tmpMod {
-		var err error
-		modFH, err = s.GetFile(ctx, s.view.modURI)
+		modFH, err := s.GetFile(ctx, s.view.modURI)
 		if err != nil {
 			return err
 		}
-		if s.view.sumURI != "" {
-			sumFH, err = s.GetFile(ctx, s.view.sumURI)
-			if err != nil {
-				return err
-			}
+		pmh, err = s.ParseModHandle(ctx, modFH)
+		if err != nil {
+			return err
 		}
 	}
-	_, inv, cleanup, err := goCommandInvocation(ctx, cfg, modFH, sumFH, verb, args)
+	_, inv, cleanup, err := goCommandInvocation(ctx, cfg, pmh, verb, args)
 	if err != nil {
 		return err
 	}
@@ -189,8 +189,13 @@
 // The given go.mod file is used to construct the temporary go.mod file, which
 // is then passed to the go command via the BuildFlags.
 // It assumes that modURI is only provided when the -modfile flag is enabled.
-func runGoCommand(ctx context.Context, cfg *packages.Config, modFH, sumFH source.FileHandle, verb string, args []string) (span.URI, *bytes.Buffer, error) {
-	tmpURI, inv, cleanup, err := goCommandInvocation(ctx, cfg, modFH, sumFH, verb, args)
+func runGoCommand(ctx context.Context, cfg *packages.Config, pmh source.ParseModHandle, tmpMod bool, verb string, args []string) (span.URI, *bytes.Buffer, error) {
+	// Don't pass in the ParseModHandle if we are not using the -modfile flag.
+	var tmpPMH source.ParseModHandle
+	if tmpMod {
+		tmpPMH = pmh
+	}
+	tmpURI, inv, cleanup, err := goCommandInvocation(ctx, cfg, tmpPMH, verb, args)
 	if err != nil {
 		return "", nil, err
 	}
@@ -202,10 +207,10 @@
 }
 
 // Assumes that modURI is only provided when the -modfile flag is enabled.
-func goCommandInvocation(ctx context.Context, cfg *packages.Config, modFH, sumFH source.FileHandle, verb string, args []string) (tmpURI span.URI, inv *gocommand.Invocation, cleanup func(), err error) {
+func goCommandInvocation(ctx context.Context, cfg *packages.Config, pmh source.ParseModHandle, verb string, args []string) (tmpURI span.URI, inv *gocommand.Invocation, cleanup func(), err error) {
 	cleanup = func() {} // fallback
-	if modFH != nil {
-		tmpURI, cleanup, err = tempModFile(modFH, sumFH)
+	if pmh != nil {
+		tmpURI, cleanup, err = tempModFile(pmh.Mod(), pmh.Sum())
 		if err != nil {
 			return "", nil, nil, err
 		}
@@ -345,10 +350,22 @@
 	}
 }
 
-func (s *snapshot) getModHandle(uri span.URI) *modHandle {
+func (s *snapshot) getModHandle(uri span.URI) *parseModHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
-	return s.modHandles[uri]
+	return s.parseModHandles[uri]
+}
+
+func (s *snapshot) getModWhyHandle() *modWhyHandle {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	return s.modWhyHandle
+}
+
+func (s *snapshot) getModUpgradeHandle() *modUpgradeHandle {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	return s.modUpgradeHandle
 }
 
 func (s *snapshot) getModTidyHandle() *modTidyHandle {
@@ -625,7 +642,7 @@
 		return fh, nil
 	}
 
-	fh, err := s.view.session.cache.GetFile(ctx, uri)
+	fh, err := s.view.session.cache.getFile(ctx, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -787,8 +804,10 @@
 		files:             make(map[span.URI]source.FileHandle),
 		workspacePackages: make(map[packageID]packagePath),
 		unloadableFiles:   make(map[span.URI]struct{}),
-		modHandles:        make(map[span.URI]*modHandle),
+		parseModHandles:   make(map[span.URI]*parseModHandle),
 		modTidyHandle:     s.modTidyHandle,
+		modUpgradeHandle:  s.modUpgradeHandle,
+		modWhyHandle:      s.modWhyHandle,
 	}
 
 	// Copy all of the FileHandles.
@@ -800,8 +819,8 @@
 		result.unloadableFiles[k] = v
 	}
 	// Copy all of the modHandles.
-	for k, v := range s.modHandles {
-		result.modHandles[k] = v
+	for k, v := range s.parseModHandles {
+		result.parseModHandles[k] = v
 	}
 
 	// transitiveIDs keeps track of transitive reverse dependencies.
@@ -827,6 +846,8 @@
 		// saved or if any of the metadata has been invalidated.
 		if invalidateMetadata || fileWasSaved(originalFH, currentFH) {
 			result.modTidyHandle = nil
+			result.modUpgradeHandle = nil
+			result.modWhyHandle = nil
 		}
 		if currentFH.Kind() == source.Mod {
 			// If the view's go.mod file's contents have changed, invalidate the metadata
@@ -836,7 +857,7 @@
 					directIDs[id] = struct{}{}
 				}
 			}
-			delete(result.modHandles, withoutURI)
+			delete(result.parseModHandles, withoutURI)
 		}
 
 		// If this is a file we don't yet know about,
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index a831c17..f04a988 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -186,7 +186,7 @@
 	defer tmpMod.Close()
 
 	tmpURI = span.URIFromPath(tmpMod.Name())
-	tmpSumName := tmpURI.Filename()[:len(tmpURI.Filename())-len("mod")] + "sum"
+	tmpSumName := sumFilename(tmpURI)
 
 	content, err := modFh.Read()
 	if err != nil {
@@ -308,7 +308,7 @@
 
 	// Get the FileHandle through the cache to avoid adding it to the snapshot
 	// and to get the file content from disk.
-	fh, err := v.session.cache.GetFile(ctx, uri)
+	fh, err := v.session.cache.getFile(ctx, uri)
 	if err != nil {
 		return err
 	}
@@ -376,12 +376,12 @@
 		// Use temporary go.mod files, but always go to disk for the contents.
 		// Rebuilding the cache is expensive, and we don't want to do it for
 		// transient changes.
-		modFH, err = v.session.cache.GetFile(ctx, v.modURI)
+		modFH, err = v.session.cache.getFile(ctx, v.modURI)
 		if err != nil {
 			return err
 		}
 		if v.sumURI != "" {
-			sumFH, err = v.session.cache.GetFile(ctx, v.sumURI)
+			sumFH, err = v.session.cache.getFile(ctx, v.sumURI)
 			if err != nil {
 				return err
 			}
@@ -396,7 +396,7 @@
 
 	// If the go.mod file has changed, clear the cache.
 	if v.modURI != "" {
-		modFH, err := v.session.cache.GetFile(ctx, v.modURI)
+		modFH, err := v.session.cache.getFile(ctx, v.modURI)
 		if err != nil {
 			return err
 		}
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index 98dc3c5..7218b3c 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -46,11 +46,11 @@
 func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
 	view := snapshot.View()
 
-	mh, err := snapshot.ModHandle(ctx, fh)
+	pmh, err := snapshot.ParseModHandle(ctx, fh)
 	if err != nil {
 		return nil, err
 	}
-	file, m, err := mh.Parse(ctx)
+	file, m, _, err := pmh.Parse(ctx)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index 41f80f5..b72dcad 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -21,25 +21,35 @@
 	ctx, done := event.Start(ctx, "mod.CodeLens", tag.URI.Of(uri))
 	defer done()
 
-	// Don't show go.mod code lenses in module mode.
-	if snapshot.View().ModFile() == "" {
+	// Only show go.mod code lenses in module mode, for the view's go.mod.
+	if modURI := snapshot.View().ModFile(); modURI == "" || modURI != uri {
 		return nil, nil
 	}
 	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return nil, err
 	}
-	mh, err := snapshot.ModHandle(ctx, fh)
+	pmh, err := snapshot.ParseModHandle(ctx, fh)
 	if err != nil {
 		return nil, err
 	}
-	f, m, upgrades, err := mh.Upgrades(ctx)
+	file, m, _, err := pmh.Parse(ctx)
 	if err != nil {
 		return nil, err
 	}
-	var codelens []protocol.CodeLens
-	var allUpgrades []string
-	for _, req := range f.Require {
+	muh, err := snapshot.ModUpgradeHandle(ctx)
+	if err != nil {
+		return nil, err
+	}
+	upgrades, err := muh.Upgrades(ctx)
+	if err != nil {
+		return nil, err
+	}
+	var (
+		codelens    []protocol.CodeLens
+		allUpgrades []string
+	)
+	for _, req := range file.Require {
 		dep := req.Mod.Path
 		latest, ok := upgrades[dep]
 		if !ok {
@@ -61,7 +71,7 @@
 		allUpgrades = append(allUpgrades, dep)
 	}
 	// If there is at least 1 upgrade, add an "Upgrade all dependencies" to the module statement.
-	if module := f.Module; len(allUpgrades) > 0 && module != nil && module.Syntax != nil {
+	if module := file.Module; len(allUpgrades) > 0 && module != nil && module.Syntax != nil {
 		// Get the range of the module directive.
 		rng, err := positionsToRange(uri, m, module.Syntax.Start, module.Syntax.End)
 		if err != nil {
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 84320dc..b07d7b2 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -29,18 +29,21 @@
 	if err != nil {
 		return nil, nil, err
 	}
-	mth, err := snapshot.ModTidyHandle(ctx, fh)
+	mth, err := snapshot.ModTidyHandle(ctx)
+	if err == source.ErrTmpModfileUnsupported {
+		return nil, nil, nil
+	}
 	if err != nil {
 		return nil, nil, err
 	}
-	_, _, missingDeps, parseErrors, err := mth.Tidy(ctx)
+	missingDeps, diagnostics, err := mth.Tidy(ctx)
 	if err != nil {
 		return nil, nil, err
 	}
 	reports := map[source.FileIdentity][]*source.Diagnostic{
 		fh.Identity(): {},
 	}
-	for _, e := range parseErrors {
+	for _, e := range diagnostics {
 		diag := &source.Diagnostic{
 			Message: e.Message,
 			Range:   e.Range,
@@ -56,17 +59,20 @@
 	return reports, missingDeps, nil
 }
 
-func SuggestedFixes(ctx context.Context, snapshot source.Snapshot, realfh source.FileHandle, diags []protocol.Diagnostic) ([]protocol.CodeAction, error) {
-	mth, err := snapshot.ModTidyHandle(ctx, realfh)
+func SuggestedFixes(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, diags []protocol.Diagnostic) ([]protocol.CodeAction, error) {
+	mth, err := snapshot.ModTidyHandle(ctx)
+	if err == source.ErrTmpModfileUnsupported {
+		return nil, nil
+	}
 	if err != nil {
 		return nil, err
 	}
-	_, _, _, parseErrors, err := mth.Tidy(ctx)
+	_, diagnostics, err := mth.Tidy(ctx)
 	if err != nil {
 		return nil, err
 	}
 	errorsMap := make(map[string][]source.Error)
-	for _, e := range parseErrors {
+	for _, e := range diagnostics {
 		if errorsMap[e.Message] == nil {
 			errorsMap[e.Message] = []source.Error{}
 		}
@@ -115,52 +121,63 @@
 	ctx, done := event.Start(ctx, "mod.SuggestedGoFixes", tag.URI.Of(uri))
 	defer done()
 
-	realfh, err := snapshot.GetFile(ctx, uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return nil, err
 	}
-	mth, err := snapshot.ModTidyHandle(ctx, realfh)
+	mth, err := snapshot.ModTidyHandle(ctx)
+	if err == source.ErrTmpModfileUnsupported {
+		return nil, nil
+	}
 	if err != nil {
 		return nil, err
 	}
-	realFile, realMapper, missingDeps, _, err := mth.Tidy(ctx)
+	missingDeps, _, err := mth.Tidy(ctx)
 	if err != nil {
 		return nil, err
 	}
 	if len(missingDeps) == 0 {
 		return nil, nil
 	}
+	pmh, err := snapshot.ParseModHandle(ctx, fh)
+	if err != nil {
+		return nil, err
+	}
+	file, m, _, err := pmh.Parse(ctx)
+	if err != nil {
+		return nil, err
+	}
 	// Get the contents of the go.mod file before we make any changes.
-	oldContents, err := realfh.Read()
+	oldContents, err := fh.Read()
 	if err != nil {
 		return nil, err
 	}
 	textDocumentEdits := make(map[string]protocol.TextDocumentEdit)
 	for dep, req := range missingDeps {
 		// Calculate the quick fix edits that need to be made to the go.mod file.
-		if err := realFile.AddRequire(req.Mod.Path, req.Mod.Version); err != nil {
+		if err := file.AddRequire(req.Mod.Path, req.Mod.Version); err != nil {
 			return nil, err
 		}
-		realFile.Cleanup()
-		newContents, err := realFile.Format()
+		file.Cleanup()
+		newContents, err := file.Format()
 		if err != nil {
 			return nil, err
 		}
 		// Reset the *modfile.File back to before we added the dependency.
-		if err := realFile.DropRequire(req.Mod.Path); err != nil {
+		if err := file.DropRequire(req.Mod.Path); err != nil {
 			return nil, err
 		}
 		// Calculate the edits to be made due to the change.
-		diff := snapshot.View().Options().ComputeEdits(realfh.URI(), string(oldContents), string(newContents))
-		edits, err := source.ToProtocolEdits(realMapper, diff)
+		diff := snapshot.View().Options().ComputeEdits(fh.URI(), string(oldContents), string(newContents))
+		edits, err := source.ToProtocolEdits(m, diff)
 		if err != nil {
 			return nil, err
 		}
 		textDocumentEdits[dep] = protocol.TextDocumentEdit{
 			TextDocument: protocol.VersionedTextDocumentIdentifier{
-				Version: realfh.Version(),
+				Version: fh.Version(),
 				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-					URI: protocol.URIFromSpanURI(realfh.URI()),
+					URI: protocol.URIFromSpanURI(fh.URI()),
 				},
 			},
 			Edits: edits,
diff --git a/internal/lsp/mod/format.go b/internal/lsp/mod/format.go
index 4aaf8ae..fdb52e4 100644
--- a/internal/lsp/mod/format.go
+++ b/internal/lsp/mod/format.go
@@ -12,11 +12,11 @@
 	ctx, done := event.Start(ctx, "mod.Format")
 	defer done()
 
-	mh, err := snapshot.ModHandle(ctx, fh)
+	pmh, err := snapshot.ParseModHandle(ctx, fh)
 	if err != nil {
 		return nil, err
 	}
-	file, m, err := mh.Parse(ctx)
+	file, m, _, err := pmh.Parse(ctx)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/mod/hover.go b/internal/lsp/mod/hover.go
index 2775dea..98b9cee 100644
--- a/internal/lsp/mod/hover.go
+++ b/internal/lsp/mod/hover.go
@@ -17,22 +17,23 @@
 func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
 	uri := snapshot.View().ModFile()
 
-	// Only get hover information on the go.mod for the view.
+	// For now, we only provide hover information for the view's go.mod file.
 	if uri == "" || fh.URI() != uri {
 		return nil, nil
 	}
+
 	ctx, done := event.Start(ctx, "mod.Hover")
 	defer done()
 
-	mh, err := snapshot.ModHandle(ctx, fh)
+	// Get the position of the cursor.
+	pmh, err := snapshot.ParseModHandle(ctx, fh)
 	if err != nil {
 		return nil, fmt.Errorf("getting modfile handle: %w", err)
 	}
-	file, m, why, err := mh.Why(ctx)
+	file, m, _, err := pmh.Parse(ctx)
 	if err != nil {
-		return nil, fmt.Errorf("running go mod why: %w", err)
+		return nil, err
 	}
-	// Get the position of the cursor.
 	spn, err := m.PointSpan(position)
 	if err != nil {
 		return nil, fmt.Errorf("computing cursor position: %w", err)
@@ -42,6 +43,7 @@
 		return nil, fmt.Errorf("computing hover range: %w", err)
 	}
 
+	// Confirm that the cursor is at the position of a require statement.
 	var req *modfile.Require
 	var startPos, endPos int
 	for _, r := range file.Require {
@@ -59,13 +61,29 @@
 			break
 		}
 	}
-	if req == nil || why == nil {
+
+	// The cursor position is not on a require statement.
+	if req == nil {
+		return nil, nil
+	}
+
+	// Get the `go mod why` results for the given file.
+	mwh, err := snapshot.ModWhyHandle(ctx)
+	if err != nil {
+		return nil, err
+	}
+	why, err := mwh.Why(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("running go mod why: %w", err)
+	}
+	if why == nil {
 		return nil, nil
 	}
 	explanation, ok := why[req.Mod.Path]
 	if !ok {
 		return nil, nil
 	}
+
 	// Get the range to highlight for the hover.
 	line, col, err := m.Converter.ToPosition(startPos)
 	if err != nil {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 64d64a2..1854bc8 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -58,13 +58,23 @@
 	// -modfile flag.
 	RunGoCommandDirect(ctx context.Context, verb string, args []string) error
 
-	// ModTidyHandle returns a ModTidyHandle for the given go.mod file handle.
-	// This function can have no data or error if there is no modfile detected.
-	ModTidyHandle(ctx context.Context, fh FileHandle) (ModTidyHandle, error)
+	// ParseModHandle is used to parse go.mod files.
+	ParseModHandle(ctx context.Context, fh FileHandle) (ParseModHandle, error)
 
-	// ModHandle returns a ModHandle for the passed in go.mod file handle.
-	// This function can have no data if there is no modfile detected.
-	ModHandle(ctx context.Context, fh FileHandle) (ModHandle, error)
+	// ModWhyHandle is used get the results of `go mod why` for a given module.
+	// It only works for go.mod files that can be parsed, hence it takes a
+	// ParseModHandle.
+	ModWhyHandle(ctx context.Context) (ModWhyHandle, error)
+
+	// ModWhyHandle is used get the possible upgrades for the dependencies of
+	// a given module. It only works for go.mod files that can be parsed, hence
+	// it takes a ParseModHandle.
+	ModUpgradeHandle(ctx context.Context) (ModUpgradeHandle, error)
+
+	// ModWhyHandle is used get the results of `go mod tidy` for a given
+	// module. It only works for go.mod files that can be parsed, hence it
+	// takes a ParseModHandle.
+	ModTidyHandle(ctx context.Context) (ModTidyHandle, error)
 
 	// PackageHandles returns the PackageHandles for the packages that this file
 	// belongs to.
@@ -291,37 +301,37 @@
 	Cached() (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error)
 }
 
-// ModHandle represents a handle to the modfile for a go.mod.
-type ModHandle interface {
-	// File returns a file handle for which to get the modfile.
-	File() FileHandle
+type ParseModHandle interface {
+	// Mod returns the file handle for the go.mod file.
+	Mod() FileHandle
 
-	// Parse returns the parsed modfile and a mapper for the go.mod file.
-	// If the file is not available, returns nil and an error.
-	Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error)
+	// Sum returns the file handle for the analogous go.sum file. It may be nil.
+	Sum() FileHandle
 
-	// Upgrades returns the parsed modfile, a mapper, and any dependency upgrades
-	// for the go.mod file. Note that this will only work if the go.mod is the view's go.mod.
-	// If the file is not available, returns nil and an error.
-	Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
-
-	// Why returns the parsed modfile, a mapper, and any explanations why a dependency should be
-	// in the go.mod file. Note that this will only work if the go.mod is the view's go.mod.
-	// If the file is not available, returns nil and an error.
-	Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
+	// Parse returns the parsed go.mod file, a column mapper, and a list of
+	// parse for the go.mod file.
+	Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, []Error, error)
 }
 
-// ModTidyHandle represents a handle to the modfile for the view.
-// Specifically for the purpose of getting diagnostics by running "go mod tidy".
+type ModUpgradeHandle interface {
+	// Upgrades returns the latest versions for each of the module's
+	// dependencies.
+	Upgrades(ctx context.Context) (map[string]string, error)
+}
+
+type ModWhyHandle interface {
+	// Why returns the results of `go mod why` for every dependency of the
+	// module.
+	Why(ctx context.Context) (map[string]string, error)
+}
+
 type ModTidyHandle interface {
-	// File returns a file handle for which to get the modfile.
-	File() FileHandle
-
-	// Tidy returns the parsed modfile, a mapper, and "go mod tidy" errors
-	// for the go.mod file. If the file is not available, returns nil and an error.
-	Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []Error, error)
+	// Tidy returns the results of `go mod tidy` for the module.
+	Tidy(ctx context.Context) (map[string]*modfile.Require, []Error, error)
 }
 
+var ErrTmpModfileUnsupported = errors.New("-modfile is unsupported for this Go version")
+
 // ParseMode controls the content of the AST produced when parsing a source file.
 type ParseMode int
 
@@ -388,14 +398,15 @@
 type FileKind int
 
 const (
+	// UnknownKind is a file type we don't know about.
+	UnknownKind = FileKind(iota)
+
 	// Go is a normal go source file.
-	Go = FileKind(iota)
+	Go
 	// Mod is a go.mod file.
 	Mod
 	// Sum is a go.sum file.
 	Sum
-	// UnknownKind is a file type we don't know about.
-	UnknownKind
 )
 
 // Analyzer represents a go/analysis analyzer with some boolean properties