internal/lsp: remove Mod handles

Continuing the massacre, remove ParseModHandle, and Mod*Handle, from the
source API.

Notably, having the snapshot available means we can simplify the go
command invocation paths a lot.

Change-Id: Ief4ef41e42f93d653f719a230004861e5e1ef70b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/244769
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index 099457f..f31fa4f 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -29,44 +29,31 @@
 
 type parseModHandle struct {
 	handle *memoize.Handle
-
-	mod, sum source.FileHandle
 }
 
 type parseModData struct {
 	memoize.NoCopy
 
-	parsed *modfile.File
-	m      *protocol.ColumnMapper
-
-	// parseErrors refers to syntax errors found in the go.mod file.
-	parseErrors []source.Error
+	parsed *source.ParsedModule
 
 	// err is any error encountered while parsing the file.
 	err error
 }
 
-func (mh *parseModHandle) Mod() source.FileHandle {
-	return mh.mod
-}
-
-func (mh *parseModHandle) Sum() source.FileHandle {
-	return mh.sum
-}
-
-func (mh *parseModHandle) Parse(ctx context.Context, s source.Snapshot) (*modfile.File, *protocol.ColumnMapper, []source.Error, error) {
+func (mh *parseModHandle) parse(ctx context.Context, s source.Snapshot) (*source.ParsedModule, error) {
 	v, err := mh.handle.Get(ctx, s.(*snapshot))
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, err
 	}
 	data := v.(*parseModData)
-	return data.parsed, data.m, data.parseErrors, data.err
+	return data.parsed, data.err
 }
 
-func (s *snapshot) ParseModHandle(ctx context.Context, modFH source.FileHandle) (source.ParseModHandle, error) {
+func (s *snapshot) ParseMod(ctx context.Context, modFH source.FileHandle) (*source.ParsedModule, error) {
 	if handle := s.getModHandle(modFH.URI()); handle != nil {
-		return handle, nil
+		return handle.parse(ctx, s)
 	}
+
 	h := s.view.session.cache.store.Bind(modFH.Identity().String(), func(ctx context.Context, _ memoize.Arg) interface{} {
 		_, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI()))
 		defer done()
@@ -80,55 +67,52 @@
 			Converter: span.NewContentConverter(modFH.URI().Filename(), contents),
 			Content:   contents,
 		}
-		parsed, err := modfile.Parse(modFH.URI().Filename(), contents, nil)
-		if err != nil {
-			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 := &parseModData{
+			parsed: &source.ParsedModule{
+				Mapper: m,
+			},
+		}
+		data.parsed.File, data.err = modfile.Parse(modFH.URI().Filename(), contents, nil)
+		if data.err != nil {
+			// Attempt to convert the error to a non-fatal parse error.
+			if parseErr, extractErr := extractModParseErrors(modFH.URI(), m, data.err, contents); extractErr == nil {
+				data.err = nil
+				data.parsed.ParseErrors = []source.Error{*parseErr}
 			}
 		}
-		return &parseModData{
-			parsed: parsed,
-			m:      m,
-		}
+		return data
 	})
+
+	pmh := &parseModHandle{handle: h}
+	s.mu.Lock()
+	s.parseModHandles[modFH.URI()] = pmh
+	s.mu.Unlock()
+
+	return pmh.parse(ctx, s)
+}
+
+func (s *snapshot) sumFH(ctx context.Context, modFH source.FileHandle) (source.FileHandle, error) {
 	// 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) {
+		var err error
+		sumFH, err = s.view.session.cache.getFile(ctx, sumURI)
+		if err != nil {
 			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,
+	_, err := sumFH.Read()
+	if err != nil {
+		return nil, err
 	}
-	return s.parseModHandles[modFH.URI()], nil
+	return sumFH, nil
 }
 
 func sumFilename(modURI span.URI) string {
-	return modURI.Filename()[:len(modURI.Filename())-len("mod")] + "sum"
+	return strings.TrimSuffix(modURI.Filename(), ".mod") + ".sum"
 }
 
 // extractModParseErrors processes the raw errors returned by modfile.Parse,
@@ -197,7 +181,7 @@
 	err error
 }
 
-func (mwh *modWhyHandle) Why(ctx context.Context, s source.Snapshot) (map[string]string, error) {
+func (mwh *modWhyHandle) why(ctx context.Context, s source.Snapshot) (map[string]string, error) {
 	v, err := mwh.handle.Get(ctx, s.(*snapshot))
 	if err != nil {
 		return nil, err
@@ -206,7 +190,7 @@
 	return data.why, data.err
 }
 
-func (s *snapshot) ModWhyHandle(ctx context.Context) (source.ModWhyHandle, error) {
+func (s *snapshot) ModWhy(ctx context.Context) (map[string]string, error) {
 	if err := s.awaitLoaded(ctx); err != nil {
 		return nil, err
 	}
@@ -214,7 +198,6 @@
 	if err != nil {
 		return nil, err
 	}
-	cfg := s.config(ctx)
 	key := modKey{
 		sessionID: s.view.session.id,
 		cfg:       hashConfig(s.config(ctx)),
@@ -228,46 +211,42 @@
 
 		snapshot := arg.(*snapshot)
 
-		pmh, err := snapshot.ParseModHandle(ctx, fh)
-		if err != nil {
-			return &modWhyData{err: err}
-		}
-
-		parsed, _, _, err := pmh.Parse(ctx, snapshot)
+		pm, err := snapshot.ParseMod(ctx, fh)
 		if err != nil {
 			return &modWhyData{err: err}
 		}
 		// No requires to explain.
-		if len(parsed.Require) == 0 {
+		if len(pm.File.Require) == 0 {
 			return &modWhyData{}
 		}
 		// Run `go mod why` on all the dependencies.
 		args := []string{"why", "-m"}
-		for _, req := range parsed.Require {
+		for _, req := range pm.File.Require {
 			args = append(args, req.Mod.Path)
 		}
-		_, stdout, err := runGoCommand(ctx, cfg, pmh, snapshot.view.tmpMod, "mod", args)
+		stdout, err := snapshot.RunGoCommand(ctx, "mod", args)
 		if err != nil {
 			return &modWhyData{err: err}
 		}
 		whyList := strings.Split(stdout.String(), "\n\n")
-		if len(whyList) != len(parsed.Require) {
+		if len(whyList) != len(pm.File.Require) {
 			return &modWhyData{
-				err: fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(parsed.Require)),
+				err: fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)),
 			}
 		}
-		why := make(map[string]string, len(parsed.Require))
-		for i, req := range parsed.Require {
+		why := make(map[string]string, len(pm.File.Require))
+		for i, req := range pm.File.Require {
 			why[req.Mod.Path] = whyList[i]
 		}
 		return &modWhyData{why: why}
 	})
+
+	mwh := &modWhyHandle{handle: h}
 	s.mu.Lock()
-	defer s.mu.Unlock()
-	s.modWhyHandle = &modWhyHandle{
-		handle: h,
-	}
-	return s.modWhyHandle, nil
+	s.modWhyHandle = mwh
+	s.mu.Unlock()
+
+	return s.modWhyHandle.why(ctx, s)
 }
 
 type modUpgradeHandle struct {
@@ -290,7 +269,7 @@
 	return data.upgrades, data.err
 }
 
-func (s *snapshot) ModUpgradeHandle(ctx context.Context) (source.ModUpgradeHandle, error) {
+func (s *snapshot) ModUpgrade(ctx context.Context) (map[string]string, error) {
 	if err := s.awaitLoaded(ctx); err != nil {
 		return nil, err
 	}
@@ -312,28 +291,24 @@
 
 		snapshot := arg.(*snapshot)
 
-		pmh, err := s.ParseModHandle(ctx, fh)
+		pm, err := s.ParseMod(ctx, fh)
 		if err != nil {
 			return &modUpgradeData{err: err}
 		}
 
-		parsed, _, _, err := pmh.Parse(ctx, snapshot)
-		if err != nil {
-			return &modUpgradeData{err: err}
-		}
 		// No requires to upgrade.
-		if len(parsed.Require) == 0 {
+		if len(pm.File.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 !snapshot.view.tmpMod || containsVendor(pmh.Mod().URI()) {
+		if !snapshot.view.tmpMod || containsVendor(fh.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, snapshot.view.tmpMod, "list", args)
+		stdout, err := snapshot.RunGoCommand(ctx, "list", args)
 		if err != nil {
 			return &modUpgradeData{err: err}
 		}
@@ -364,12 +339,12 @@
 			upgrades: upgrades,
 		}
 	})
+	muh := &modUpgradeHandle{handle: h}
 	s.mu.Lock()
-	defer s.mu.Unlock()
-	s.modUpgradeHandle = &modUpgradeHandle{
-		handle: h,
-	}
-	return s.modUpgradeHandle, nil
+	s.modUpgradeHandle = muh
+	s.mu.Unlock()
+
+	return s.modUpgradeHandle.Upgrades(ctx, s)
 }
 
 // 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 3a96fc0..2ecfc77 100644
--- a/internal/lsp/cache/mod_tidy.go
+++ b/internal/lsp/cache/mod_tidy.go
@@ -19,7 +19,6 @@
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/memoize"
-	"golang.org/x/tools/internal/packagesinternal"
 	"golang.org/x/tools/internal/span"
 )
 
@@ -34,57 +33,32 @@
 
 type modTidyHandle struct {
 	handle *memoize.Handle
-
-	pmh source.ParseModHandle
 }
 
 type modTidyData struct {
 	memoize.NoCopy
 
-	// tidiedContent is the content of the tidied file.
-	tidiedContent []byte
-
-	// diagnostics are any errors and associated suggested fixes for
-	// the go.mod file.
-	diagnostics []source.Error
-
-	err error
+	tidied *source.TidiedModule
+	err    error
 }
 
-func (mth *modTidyHandle) ParseModHandle() source.ParseModHandle {
-	return mth.pmh
-}
-
-func (mth *modTidyHandle) Tidy(ctx context.Context, s source.Snapshot) ([]source.Error, error) {
+func (mth *modTidyHandle) tidy(ctx context.Context, s source.Snapshot) (*source.TidiedModule, error) {
 	v, err := mth.handle.Get(ctx, s.(*snapshot))
 	if err != nil {
 		return nil, err
 	}
 	data := v.(*modTidyData)
-	return data.diagnostics, data.err
+	return data.tidied, data.err
 }
 
-func (mth *modTidyHandle) TidiedContent(ctx context.Context, s source.Snapshot) ([]byte, error) {
-	v, err := mth.handle.Get(ctx, s.(*snapshot))
-	if err != nil {
-		return nil, err
-	}
-	data := v.(*modTidyData)
-	return data.tidiedContent, data.err
-}
-
-func (s *snapshot) ModTidyHandle(ctx context.Context) (source.ModTidyHandle, error) {
+func (s *snapshot) ModTidy(ctx context.Context) (*source.TidiedModule, error) {
 	if !s.view.tmpMod {
 		return nil, source.ErrTmpModfileUnsupported
 	}
 	if handle := s.getModTidyHandle(); handle != nil {
-		return handle, nil
+		return handle.tidy(ctx, s)
 	}
-	fh, err := s.GetFile(ctx, s.view.modURI)
-	if err != nil {
-		return nil, err
-	}
-	pmh, err := s.ParseModHandle(ctx, fh)
+	modFH, err := s.GetFile(ctx, s.view.modURI)
 	if err != nil {
 		return nil, err
 	}
@@ -111,7 +85,7 @@
 		view:            s.view.root.Filename(),
 		imports:         importHash,
 		unsavedOverlays: overlayHash,
-		gomod:           pmh.Mod().Identity().String(),
+		gomod:           modFH.Identity().String(),
 		cfg:             hashConfig(cfg),
 	}
 	h := s.view.session.cache.store.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
@@ -119,21 +93,26 @@
 		defer done()
 
 		snapshot := arg.(*snapshot)
-		original, m, parseErrors, err := pmh.Parse(ctx, snapshot)
-		if err != nil || len(parseErrors) > 0 {
+		pm, err := snapshot.ParseMod(ctx, modFH)
+		if err != nil {
+			return &modTidyData{err: err}
+		}
+		if len(pm.ParseErrors) > 0 {
 			return &modTidyData{
-				diagnostics: parseErrors,
-				err:         err,
+				tidied: &source.TidiedModule{
+					Parsed: pm,
+				},
+				err: fmt.Errorf("could not parse module to tidy: %v", pm.ParseErrors),
 			}
 		}
-		tmpURI, inv, cleanup, err := goCommandInvocation(ctx, cfg, pmh, "mod", []string{"tidy"})
+		tmpURI, runner, inv, cleanup, err := snapshot.goCommandInvocation(ctx, true, "mod", []string{"tidy"})
 		if err != nil {
 			return &modTidyData{err: err}
 		}
 		// Keep the temporary go.mod file around long enough to parse it.
 		defer cleanup()
 
-		if _, err := packagesinternal.GetGoCmdRunner(cfg).Run(ctx, *inv); err != nil {
+		if _, err := runner.Run(ctx, *inv); err != nil {
 			return &modTidyData{err: err}
 		}
 		// Go directly to disk to get the temporary mod file, since it is
@@ -150,9 +129,9 @@
 		}
 		// Get the dependencies that are different between the original and
 		// ideal go.mod files.
-		unusedDeps := make(map[string]*modfile.Require, len(original.Require))
+		unusedDeps := make(map[string]*modfile.Require, len(pm.File.Require))
 		missingDeps := make(map[string]*modfile.Require, len(ideal.Require))
-		for _, req := range original.Require {
+		for _, req := range pm.File.Require {
 			unusedDeps[req.Mod.Path] = req
 		}
 		for _, req := range ideal.Require {
@@ -166,7 +145,7 @@
 		// First, compute any errors specific to the go.mod file. These include
 		// unused dependencies and modules with incorrect // indirect comments.
 		/// Both the diagnostic and the fix will appear on the go.mod file.
-		modRequireErrs, err := modRequireErrors(m, missingDeps, unusedDeps, options)
+		modRequireErrs, err := modRequireErrors(pm.Mapper, missingDeps, unusedDeps, options)
 		if err != nil {
 			return &modTidyData{err: err}
 		}
@@ -179,22 +158,25 @@
 		// go.mod file. The fixes will be for the go.mod file, but the
 		// diagnostics should appear on the import statements in the Go or
 		// go.mod files.
-		missingModuleErrs, err := missingModuleErrors(ctx, snapshot, m, workspacePkgs, ideal.Require, missingDeps, original, options)
+		missingModuleErrs, err := missingModuleErrors(ctx, snapshot, pm.Mapper, workspacePkgs, ideal.Require, missingDeps, pm.File, options)
 		if err != nil {
 			return &modTidyData{err: err}
 		}
 		return &modTidyData{
-			tidiedContent: tempContents,
-			diagnostics:   append(modRequireErrs, missingModuleErrs...),
+			tidied: &source.TidiedModule{
+				Parsed:        pm,
+				TidiedContent: tempContents,
+				Errors:        append(modRequireErrs, missingModuleErrs...),
+			},
 		}
 	})
+
+	mth := &modTidyHandle{handle: h}
 	s.mu.Lock()
-	defer s.mu.Unlock()
-	s.modTidyHandle = &modTidyHandle{
-		handle: h,
-		pmh:    pmh,
-	}
-	return s.modTidyHandle, nil
+	s.modTidyHandle = mth
+	s.mu.Unlock()
+
+	return mth.tidy(ctx, s)
 }
 
 func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index d9fb962..6ea8e4f 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -142,83 +142,55 @@
 }
 
 func (s *snapshot) RunGoCommandDirect(ctx context.Context, verb string, args []string) error {
-	cfg := s.config(ctx)
-	_, _, 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 pmh source.ParseModHandle
-	if s.view.tmpMod {
-		modFH, err := s.GetFile(ctx, s.view.modURI)
-		if err != nil {
-			return nil, err
-		}
-		pmh, err = s.ParseModHandle(ctx, modFH)
-		if err != nil {
-			return nil, err
-		}
-	}
-	_, 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 pmh source.ParseModHandle
-	if s.view.tmpMod {
-		modFH, err := s.GetFile(ctx, s.view.modURI)
-		if err != nil {
-			return err
-		}
-		pmh, err = s.ParseModHandle(ctx, modFH)
-		if err != nil {
-			return err
-		}
-	}
-	_, inv, cleanup, err := goCommandInvocation(ctx, cfg, pmh, verb, args)
+	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, false, verb, args)
 	if err != nil {
 		return err
 	}
 	defer cleanup()
 
-	runner := packagesinternal.GetGoCmdRunner(cfg)
-	return runner.RunPiped(ctx, *inv, stdout, stderr)
+	_, err = runner.Run(ctx, *inv)
+	return err
 }
 
-// runGoCommand runs the given go command with the given config.
-// 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, 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)
+func (s *snapshot) RunGoCommand(ctx context.Context, verb string, args []string) (*bytes.Buffer, error) {
+	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, true, verb, args)
 	if err != nil {
-		return "", nil, err
+		return nil, err
 	}
 	defer cleanup()
 
-	runner := packagesinternal.GetGoCmdRunner(cfg)
-	stdout, err := runner.Run(ctx, *inv)
-	return tmpURI, stdout, err
+	return runner.Run(ctx, *inv)
+}
+
+func (s *snapshot) RunGoCommandPiped(ctx context.Context, verb string, args []string, stdout, stderr io.Writer) error {
+	_, runner, inv, cleanup, err := s.goCommandInvocation(ctx, true, verb, args)
+	if err != nil {
+		return err
+	}
+	defer cleanup()
+	return runner.RunPiped(ctx, *inv, stdout, stderr)
 }
 
 // Assumes that modURI is only provided when the -modfile flag is enabled.
-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) {
+func (s *snapshot) goCommandInvocation(ctx context.Context, allowTempModfile bool, verb string, args []string) (tmpURI span.URI, runner *gocommand.Runner, inv *gocommand.Invocation, cleanup func(), err error) {
 	cleanup = func() {} // fallback
-	if pmh != nil {
-		tmpURI, cleanup, err = tempModFile(pmh.Mod(), pmh.Sum())
+	cfg := s.config(ctx)
+	if allowTempModfile && s.view.tmpMod {
+		modFH, err := s.GetFile(ctx, s.view.modURI)
 		if err != nil {
-			return "", nil, nil, err
+			return "", nil, nil, cleanup, err
+		}
+		// Use the go.sum if it happens to be available.
+		sumFH, _ := s.sumFH(ctx, modFH)
+
+		tmpURI, cleanup, err = tempModFile(modFH, sumFH)
+		if err != nil {
+			return "", nil, nil, cleanup, err
 		}
 		cfg.BuildFlags = append(cfg.BuildFlags, fmt.Sprintf("-modfile=%s", tmpURI.Filename()))
 	}
-	return tmpURI, &gocommand.Invocation{
+	runner = packagesinternal.GetGoCmdRunner(cfg)
+	return tmpURI, runner, &gocommand.Invocation{
 		Verb:       verb,
 		Args:       args,
 		Env:        cfg.Env,
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index cbe70ec..72674d9 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -562,15 +562,11 @@
 	if err != nil {
 		return nil, err
 	}
-	pmh, err := v.Snapshot().ParseModHandle(ctx, fh)
+	pm, err := v.Snapshot().ParseMod(ctx, fh)
 	if err != nil {
 		return nil, err
 	}
-	parsed, _, _, err := pmh.Parse(ctx, v.Snapshot())
-	if err != nil {
-		return nil, err
-	}
-	for _, replace := range parsed.Replace {
+	for _, replace := range pm.File.Replace {
 		dirs = append(dirs, replace.New.Path)
 	}
 	return dirs, nil
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index bd19f55..850a466 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -454,20 +454,19 @@
 }
 
 func moduleQuickFixes(ctx context.Context, snapshot source.Snapshot, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
-	mth, err := snapshot.ModTidyHandle(ctx)
+	modFH, err := snapshot.GetFile(ctx, snapshot.View().ModFile())
+	if err != nil {
+		return nil, err
+	}
+	tidied, err := snapshot.ModTidy(ctx)
 	if err == source.ErrTmpModfileUnsupported {
 		return nil, nil
 	}
 	if err != nil {
 		return nil, err
 	}
-	errors, err := mth.Tidy(ctx, snapshot)
-	if err != nil {
-		return nil, err
-	}
-	pmh := mth.ParseModHandle()
 	var quickFixes []protocol.CodeAction
-	for _, e := range errors {
+	for _, e := range tidied.Errors {
 		var diag *protocol.Diagnostic
 		for _, d := range diagnostics {
 			if sameDiagnostic(d, e) {
@@ -486,14 +485,14 @@
 				Edit:        protocol.WorkspaceEdit{},
 			}
 			for uri, edits := range fix.Edits {
-				if uri != pmh.Mod().URI() {
+				if uri != modFH.URI() {
 					continue
 				}
 				action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, protocol.TextDocumentEdit{
 					TextDocument: protocol.VersionedTextDocumentIdentifier{
-						Version: pmh.Mod().Version(),
+						Version: modFH.Version(),
 						TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-							URI: protocol.URIFromSpanURI(pmh.Mod().URI()),
+							URI: protocol.URIFromSpanURI(modFH.URI()),
 						},
 					},
 					Edits: edits,
@@ -510,25 +509,21 @@
 }
 
 func goModTidy(ctx context.Context, snapshot source.Snapshot) (*protocol.CodeAction, error) {
-	mth, err := snapshot.ModTidyHandle(ctx)
+	tidied, err := snapshot.ModTidy(ctx)
 	if err != nil {
 		return nil, err
 	}
-	uri := mth.ParseModHandle().Mod().URI()
-	_, m, _, err := mth.ParseModHandle().Parse(ctx, snapshot)
+	modFH, err := snapshot.GetFile(ctx, snapshot.View().ModFile())
 	if err != nil {
 		return nil, err
 	}
-	left, err := mth.ParseModHandle().Mod().Read()
+	left, err := modFH.Read()
 	if err != nil {
 		return nil, err
 	}
-	right, err := mth.TidiedContent(ctx, snapshot)
-	if err != nil {
-		return nil, err
-	}
-	edits := snapshot.View().Options().ComputeEdits(uri, string(left), string(right))
-	protocolEdits, err := source.ToProtocolEdits(m, edits)
+	right := tidied.TidiedContent
+	edits := snapshot.View().Options().ComputeEdits(modFH.URI(), string(left), string(right))
+	protocolEdits, err := source.ToProtocolEdits(tidied.Parsed.Mapper, edits)
 	if err != nil {
 		return nil, err
 	}
@@ -538,9 +533,9 @@
 		Edit: protocol.WorkspaceEdit{
 			DocumentChanges: []protocol.TextDocumentEdit{{
 				TextDocument: protocol.VersionedTextDocumentIdentifier{
-					Version: mth.ParseModHandle().Mod().Version(),
+					Version: modFH.Version(),
 					TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-						URI: protocol.URIFromSpanURI(uri),
+						URI: protocol.URIFromSpanURI(modFH.URI()),
 					},
 				},
 				Edits: protocolEdits,
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index a56e673..35caccc 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -46,16 +46,12 @@
 func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
 	view := snapshot.View()
 
-	pmh, err := snapshot.ParseModHandle(ctx, fh)
-	if err != nil {
-		return nil, err
-	}
-	file, m, _, err := pmh.Parse(ctx, snapshot)
+	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		return nil, err
 	}
 	var links []protocol.DocumentLink
-	for _, req := range file.Require {
+	for _, req := range pm.File.Require {
 		if req.Syntax == nil {
 			continue
 		}
@@ -65,7 +61,7 @@
 		}
 		dep := []byte(req.Mod.Path)
 		s, e := req.Syntax.Start.Byte, req.Syntax.End.Byte
-		i := bytes.Index(m.Content[s:e], dep)
+		i := bytes.Index(pm.Mapper.Content[s:e], dep)
 		if i == -1 {
 			continue
 		}
@@ -73,25 +69,25 @@
 		// dependency within the require statement.
 		start, end := token.Pos(s+i), token.Pos(s+i+len(dep))
 		target := fmt.Sprintf("https://%s/mod/%s", view.Options().LinkTarget, req.Mod.String())
-		l, err := toProtocolLink(view, m, target, start, end, source.Mod)
+		l, err := toProtocolLink(view, pm.Mapper, target, start, end, source.Mod)
 		if err != nil {
 			return nil, err
 		}
 		links = append(links, l)
 	}
 	// TODO(ridersofrohan): handle links for replace and exclude directives.
-	if syntax := file.Syntax; syntax == nil {
+	if syntax := pm.File.Syntax; syntax == nil {
 		return links, nil
 	}
 	// Get all the links that are contained in the comments of the file.
-	for _, expr := range file.Syntax.Stmt {
+	for _, expr := range pm.File.Syntax.Stmt {
 		comments := expr.Comment()
 		if comments == nil {
 			continue
 		}
 		for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} {
 			for _, comment := range section {
-				l, err := findLinksInString(ctx, view, comment.Token, token.Pos(comment.Start.Byte), m, source.Mod)
+				l, err := findLinksInString(ctx, view, comment.Token, token.Pos(comment.Start.Byte), pm.Mapper, source.Mod)
 				if err != nil {
 					return nil, err
 				}
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index 5f0a0b4..13afd61 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -28,19 +28,11 @@
 	if err != nil {
 		return nil, err
 	}
-	pmh, err := snapshot.ParseModHandle(ctx, fh)
+	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		return nil, err
 	}
-	file, m, _, err := pmh.Parse(ctx, snapshot)
-	if err != nil {
-		return nil, err
-	}
-	muh, err := snapshot.ModUpgradeHandle(ctx)
-	if err != nil {
-		return nil, err
-	}
-	upgrades, err := muh.Upgrades(ctx, snapshot)
+	upgrades, err := snapshot.ModUpgrade(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -48,14 +40,14 @@
 		codelens    []protocol.CodeLens
 		allUpgrades []string
 	)
-	for _, req := range file.Require {
+	for _, req := range pm.File.Require {
 		dep := req.Mod.Path
 		latest, ok := upgrades[dep]
 		if !ok {
 			continue
 		}
 		// Get the range of the require directive.
-		rng, err := positionsToRange(uri, m, req.Syntax.Start, req.Syntax.End)
+		rng, err := positionsToRange(uri, pm.Mapper, req.Syntax.Start, req.Syntax.End)
 		if err != nil {
 			return nil, err
 		}
@@ -74,9 +66,9 @@
 		allUpgrades = append(allUpgrades, dep)
 	}
 	// If there is at least 1 upgrade, add an "Upgrade all dependencies" to the module statement.
-	if module := file.Module; len(allUpgrades) > 0 && module != nil && module.Syntax != nil {
+	if module := pm.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)
+		rng, err := positionsToRange(uri, pm.Mapper, module.Syntax.Start, module.Syntax.End)
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 878294d..43fd95e 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -34,7 +34,7 @@
 	if err != nil {
 		return nil, err
 	}
-	mth, err := snapshot.ModTidyHandle(ctx)
+	tidied, err := snapshot.ModTidy(ctx)
 	if err == source.ErrTmpModfileUnsupported {
 		return nil, nil
 	}
@@ -44,11 +44,7 @@
 	if err != nil {
 		return nil, err
 	}
-	diagnostics, err := mth.Tidy(ctx, snapshot)
-	if err != nil {
-		return nil, err
-	}
-	for _, e := range diagnostics {
+	for _, e := range tidied.Errors {
 		diag := &source.Diagnostic{
 			Message: e.Message,
 			Range:   e.Range,
@@ -98,16 +94,12 @@
 			break
 		}
 	}
-	pmh, err := snapshot.ParseModHandle(ctx, fh)
-	if err != nil {
-		return nil, err
-	}
-	parsed, m, _, err := pmh.Parse(ctx, snapshot)
+	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		return nil, err
 	}
 	toDiagnostic := func(line *modfile.Line) (*source.Diagnostic, error) {
-		rng, err := rangeFromPositions(fh.URI(), m, line.Start, line.End)
+		rng, err := rangeFromPositions(fh.URI(), pm.Mapper, line.Start, line.End)
 		if err != nil {
 			return nil, err
 		}
@@ -119,19 +111,19 @@
 	}
 	// Check if there are any require, exclude, or replace statements that
 	// match this module version.
-	for _, req := range parsed.Require {
+	for _, req := range pm.File.Require {
 		if req.Mod != v {
 			continue
 		}
 		return toDiagnostic(req.Syntax)
 	}
-	for _, ex := range parsed.Exclude {
+	for _, ex := range pm.File.Exclude {
 		if ex.Mod != v {
 			continue
 		}
 		return toDiagnostic(ex.Syntax)
 	}
-	for _, rep := range parsed.Replace {
+	for _, rep := range pm.File.Replace {
 		if rep.New != v && rep.Old != v {
 			continue
 		}
@@ -139,7 +131,7 @@
 	}
 	// No match for the module path was found in the go.mod file.
 	// Show the error on the module declaration.
-	return toDiagnostic(parsed.Module.Syntax)
+	return toDiagnostic(pm.File.Module.Syntax)
 }
 
 func rangeFromPositions(uri span.URI, m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
diff --git a/internal/lsp/mod/format.go b/internal/lsp/mod/format.go
index 5d73f04..da97ca2 100644
--- a/internal/lsp/mod/format.go
+++ b/internal/lsp/mod/format.go
@@ -12,19 +12,15 @@
 	ctx, done := event.Start(ctx, "mod.Format")
 	defer done()
 
-	pmh, err := snapshot.ParseModHandle(ctx, fh)
+	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		return nil, err
 	}
-	file, m, _, err := pmh.Parse(ctx, snapshot)
-	if err != nil {
-		return nil, err
-	}
-	formatted, err := file.Format()
+	formatted, err := pm.File.Format()
 	if err != nil {
 		return nil, err
 	}
 	// Calculate the edits to be made due to the change.
-	diff := snapshot.View().Options().ComputeEdits(fh.URI(), string(m.Content), string(formatted))
-	return source.ToProtocolEdits(m, diff)
+	diff := snapshot.View().Options().ComputeEdits(fh.URI(), string(pm.Mapper.Content), string(formatted))
+	return source.ToProtocolEdits(pm.Mapper, diff)
 }
diff --git a/internal/lsp/mod/hover.go b/internal/lsp/mod/hover.go
index b664714..f0fa26b 100644
--- a/internal/lsp/mod/hover.go
+++ b/internal/lsp/mod/hover.go
@@ -26,19 +26,15 @@
 	defer done()
 
 	// Get the position of the cursor.
-	pmh, err := snapshot.ParseModHandle(ctx, fh)
+	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		return nil, fmt.Errorf("getting modfile handle: %w", err)
 	}
-	file, m, _, err := pmh.Parse(ctx, snapshot)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.PointSpan(position)
+	spn, err := pm.Mapper.PointSpan(position)
 	if err != nil {
 		return nil, fmt.Errorf("computing cursor position: %w", err)
 	}
-	hoverRng, err := spn.Range(m.Converter)
+	hoverRng, err := spn.Range(pm.Mapper.Converter)
 	if err != nil {
 		return nil, fmt.Errorf("computing hover range: %w", err)
 	}
@@ -46,10 +42,10 @@
 	// 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 {
+	for _, r := range pm.File.Require {
 		dep := []byte(r.Mod.Path)
 		s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte
-		i := bytes.Index(m.Content[s:e], dep)
+		i := bytes.Index(pm.Mapper.Content[s:e], dep)
 		if i == -1 {
 			continue
 		}
@@ -68,37 +64,30 @@
 	}
 
 	// Get the `go mod why` results for the given file.
-	mwh, err := snapshot.ModWhyHandle(ctx)
+	why, err := snapshot.ModWhy(ctx)
 	if err != nil {
 		return nil, err
 	}
-	why, err := mwh.Why(ctx, snapshot)
-	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)
+	line, col, err := pm.Mapper.Converter.ToPosition(startPos)
 	if err != nil {
 		return nil, err
 	}
 	start := span.NewPoint(line, col, startPos)
 
-	line, col, err = m.Converter.ToPosition(endPos)
+	line, col, err = pm.Mapper.Converter.ToPosition(endPos)
 	if err != nil {
 		return nil, err
 	}
 	end := span.NewPoint(line, col, endPos)
 
 	spn = span.New(fh.URI(), start, end)
-	rng, err := m.Range(spn)
+	rng, err := pm.Mapper.Range(spn)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index db76226..1aed9f5 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -74,23 +74,17 @@
 	// -modfile flag.
 	RunGoCommandDirect(ctx context.Context, verb string, args []string) error
 
-	// ParseModHandle is used to parse go.mod files.
-	ParseModHandle(ctx context.Context, fh FileHandle) (ParseModHandle, error)
+	// ParseMod is used to parse go.mod files.
+	ParseMod(ctx context.Context, fh FileHandle) (*ParsedModule, 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)
+	// ModWhy returns the results of `go mod why` for the snapshot's module.
+	ModWhy(ctx context.Context) (map[string]string, 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)
+	// ModUpgrade returns the possible updates for the snapshot's module.
+	ModUpgrade(ctx context.Context) (map[string]string, 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)
+	// ModTidy returns the results of `go mod tidy` for the snapshot's module.
+	ModTidy(ctx context.Context) (*TidiedModule, error)
 
 	// PackagesForFile returns the packages that this file belongs to.
 	PackagesForFile(ctx context.Context, uri span.URI) ([]Package, error)
@@ -183,6 +177,7 @@
 	ParsedFile() *ParsedGoFile
 }
 
+// A ParsedGoFile contains the results of parsing a Go file.
 type ParsedGoFile struct {
 	memoize.NoCopy
 
@@ -197,6 +192,27 @@
 	ParseErr error
 }
 
+// A ParsedModule contains the results of parsing a go.mod file.
+type ParsedModule struct {
+	memoize.NoCopy
+
+	File        *modfile.File
+	Mapper      *protocol.ColumnMapper
+	ParseErrors []Error
+}
+
+// A TidedModule contains the results of running `go mod tidy` on a module.
+type TidiedModule struct {
+	memoize.NoCopy
+
+	// The parsed module, which is guaranteed to have parsed successfully.
+	Parsed *ParsedModule
+	// Diagnostics representing changes made by `go mod tidy`.
+	Errors []Error
+	// The bytes of the go.mod file after it was tidied.
+	TidiedContent []byte
+}
+
 // Session represents a single connection from a client.
 // This is the level at which things like open files are maintained on behalf
 // of the client.
@@ -319,41 +335,6 @@
 	GetFile(ctx context.Context, uri span.URI) (FileHandle, error)
 }
 
-type ParseModHandle interface {
-	// Mod returns the file handle for the go.mod file.
-	Mod() FileHandle
-
-	// Sum returns the file handle for the analogous go.sum file. It may be nil.
-	Sum() FileHandle
-
-	// Parse returns the parsed go.mod file, a column mapper, and a list of
-	// parse for the go.mod file.
-	Parse(ctx context.Context, snapshot Snapshot) (*modfile.File, *protocol.ColumnMapper, []Error, error)
-}
-
-type ModUpgradeHandle interface {
-	// Upgrades returns the latest versions for each of the module's
-	// dependencies.
-	Upgrades(ctx context.Context, snapshot Snapshot) (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, snapshot Snapshot) (map[string]string, error)
-}
-
-type ModTidyHandle interface {
-	// Mod is the ParseModHandle associated with the go.mod file being tidied.
-	ParseModHandle() ParseModHandle
-
-	// Tidy returns the results of `go mod tidy` for the module.
-	Tidy(ctx context.Context, snapshot Snapshot) ([]Error, error)
-
-	// TidiedContent is the content of the tidied go.mod file.
-	TidiedContent(ctx context.Context, snapshot Snapshot) ([]byte, 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.