internal/lsp: run type error analyzers as part of diagnostics

Type error analyzers can be viewed as enhancing type errors, rather
than analyzers in their own right. Create a source.DiagnosePackage
function that combines the list/parse/typecheck diagnostics with type
error analyzers. This allows us to remove some special cases from the
analysis path, and is a first step in removing all the special
handling for analysis quick fixes.

Along the way:
Pass pointers to source.Analyzer after I spent half an hour chasing a
loop capture bug. Spend a further 2-3 hours chasing slowdown in the
command tests as a result.

Move Unnecessary tag generation into diagnostic creation rather than
as a mutating post-processing step that required cloning diagnostics.

Change-Id: Id246667a9dcf484dc79516f92d5524261c435794
Reviewed-on: https://go-review.googlesource.com/c/tools/+/297879
Trust: Heschi Kreinick <heschi@google.com>
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Heschi Kreinick <heschi@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go
index d6bd1ab..ed42647 100644
--- a/gopls/doc/generate.go
+++ b/gopls/doc/generate.go
@@ -91,7 +91,7 @@
 	for _, c := range api.Commands {
 		c.Command = command.ID(c.Command)
 	}
-	for _, m := range []map[string]source.Analyzer{
+	for _, m := range []map[string]*source.Analyzer{
 		defaults.DefaultAnalyzers,
 		defaults.TypeErrorAnalyzers,
 		defaults.ConvenienceAnalyzers,
@@ -464,7 +464,7 @@
 	return lenses
 }
 
-func loadAnalyzers(m map[string]source.Analyzer) []*source.AnalyzerJSON {
+func loadAnalyzers(m map[string]*source.Analyzer) []*source.AnalyzerJSON {
 	var sorted []string
 	for _, a := range m {
 		sorted = append(sorted, a.Analyzer.Name)
diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
index 370a034..75ba7de 100644
--- a/gopls/internal/regtest/diagnostics/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go
@@ -1252,7 +1252,7 @@
 	})
 }
 
-func TestStaticcheckDiagnostic(t *testing.T) {
+func TestSimplifyCompositeLitDiagnostic(t *testing.T) {
 	const files = `
 -- go.mod --
 module mod.com
@@ -1277,8 +1277,16 @@
 		EditorConfig{EnableStaticcheck: true},
 	).Run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("main.go")
-		// Staticcheck should generate a diagnostic to simplify this literal.
-		env.Await(env.DiagnosticAtRegexp("main.go", `t{"msg"}`))
+		var d protocol.PublishDiagnosticsParams
+		env.Await(OnceMet(
+			env.DiagnosticAtRegexpWithMessage("main.go", `t{"msg"}`, "redundant type"),
+			ReadDiagnostics("main.go", &d),
+		))
+		if tags := d.Diagnostics[0].Tags; len(tags) == 0 || tags[0] != protocol.Unnecessary {
+			t.Errorf("wanted Unnecessary tag on diagnostic, got %v", tags)
+		}
+		env.ApplyQuickFixes("main.go", d.Diagnostics)
+		env.Await(EmptyDiagnostics("main.go"))
 	})
 }
 
diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go
index 4f97c01..395bfd8 100644
--- a/gopls/internal/regtest/misc/fix_test.go
+++ b/gopls/internal/regtest/misc/fix_test.go
@@ -60,3 +60,28 @@
 		}
 	})
 }
+
+func TestFillReturns(t *testing.T) {
+	const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- main.go --
+package main
+
+func Foo() error {
+	return
+}
+`
+	Run(t, files, func(t *testing.T, env *Env) {
+		env.OpenFile("main.go")
+		var d protocol.PublishDiagnosticsParams
+		env.Await(OnceMet(
+			env.DiagnosticAtRegexpWithMessage("main.go", `return`, "wrong number of return values"),
+			ReadDiagnostics("main.go", &d),
+		))
+		env.ApplyQuickFixes("main.go", d.Diagnostics)
+		env.Await(EmptyDiagnostics("main.go"))
+	})
+}
diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go
index a06e2db..92af4f1 100644
--- a/internal/lsp/cache/analysis.go
+++ b/internal/lsp/cache/analysis.go
@@ -18,17 +18,21 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/lsp/debug/tag"
-	"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/span"
 	errors "golang.org/x/xerrors"
 )
 
-func (s *snapshot) Analyze(ctx context.Context, id string, analyzers ...*analysis.Analyzer) ([]*source.Diagnostic, error) {
+func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) {
 	var roots []*actionHandle
 
 	for _, a := range analyzers {
-		ah, err := s.actionHandle(ctx, packageID(id), a)
+
+		if !a.IsEnabled(s.view) {
+			continue
+		}
+		ah, err := s.actionHandle(ctx, packageID(id), a.Analyzer)
 		if err != nil {
 			return nil, err
 		}
@@ -324,7 +328,7 @@
 	analysisinternal.SetTypeErrors(pass, pkg.typeErrors)
 
 	if pkg.IsIllTyped() {
-		data.err = errors.Errorf("analysis skipped due to errors in package: %v", pkg.GetDiagnostics())
+		data.err = errors.Errorf("analysis skipped due to errors in package")
 		return data
 	}
 	data.result, data.err = pass.Analyzer.Run(pass)
@@ -348,7 +352,7 @@
 	}
 
 	for _, diag := range diagnostics {
-		srcDiags, err := analysisDiagnosticDiagnostics(ctx, snapshot, pkg, protocol.SeverityWarning, diag)
+		srcDiags, err := analysisDiagnosticDiagnostics(ctx, snapshot, pkg, analyzer, diag)
 		if err != nil {
 			event.Error(ctx, "unable to compute analysis error position", err, tag.Category.Of(diag.Category), tag.Package.Of(pkg.ID()))
 			continue
@@ -391,3 +395,38 @@
 	}
 	return t
 }
+
+func (s *snapshot) DiagnosePackage(ctx context.Context, spkg source.Package) (map[span.URI][]*source.Diagnostic, error) {
+	pkg := spkg.(*pkg)
+	// Apply type error analyzers. They augment type error diagnostics with their own fixes.
+	var analyzers []*source.Analyzer
+	for _, a := range s.View().Options().TypeErrorAnalyzers {
+		analyzers = append(analyzers, a)
+	}
+	var errorAnalyzerDiag []*source.Diagnostic
+	if pkg.hasTypeErrors {
+		var err error
+		errorAnalyzerDiag, err = s.Analyze(ctx, pkg.ID(), analyzers)
+		if err != nil {
+			return nil, err
+		}
+	}
+	diags := map[span.URI][]*source.Diagnostic{}
+	for _, diag := range pkg.diagnostics {
+		for _, eaDiag := range errorAnalyzerDiag {
+			if eaDiag.URI == diag.URI && eaDiag.Range == diag.Range && eaDiag.Message == diag.Message {
+				// Type error analyzers just add fixes and tags. Make a copy,
+				// since we don't own either, and overwrite.
+				// The analyzer itself can't do this merge because
+				// analysis.Diagnostic doesn't have all the fields, and Analyze
+				// can't because it doesn't have the type error, notably its code.
+				clone := *diag
+				clone.SuggestedFixes = eaDiag.SuggestedFixes
+				clone.Tags = eaDiag.Tags
+				diag = &clone
+			}
+		}
+		diags[diag.URI] = append(diags[diag.URI], diag)
+	}
+	return diags, nil
+}
diff --git a/internal/lsp/cache/errors.go b/internal/lsp/cache/errors.go
index 20f35a6..072d78f 100644
--- a/internal/lsp/cache/errors.go
+++ b/internal/lsp/cache/errors.go
@@ -17,6 +17,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
@@ -139,7 +140,16 @@
 	return []*source.Diagnostic{diag}, nil
 }
 
-func analysisDiagnosticDiagnostics(ctx context.Context, snapshot *snapshot, pkg *pkg, severity protocol.DiagnosticSeverity, e *analysis.Diagnostic) ([]*source.Diagnostic, error) {
+func analysisDiagnosticDiagnostics(ctx context.Context, snapshot *snapshot, pkg *pkg, a *analysis.Analyzer, e *analysis.Diagnostic) ([]*source.Diagnostic, error) {
+	var srcAnalyzer *source.Analyzer
+	// Find the analyzer that generated this diagnostic.
+	for _, sa := range source.EnabledAnalyzers(snapshot) {
+		if a == sa.Analyzer {
+			srcAnalyzer = sa
+			break
+		}
+	}
+
 	spn, err := span.NewRange(snapshot.FileSet(), e.Pos, e.End).Span()
 	if err != nil {
 		return nil, err
@@ -152,11 +162,22 @@
 	if err != nil {
 		return nil, err
 	}
+	if srcAnalyzer.Fix != "" {
+		cmd, err := command.NewApplyFixCommand(e.Message, command.ApplyFixArgs{
+			URI:   protocol.URIFromSpanURI(spn.URI()),
+			Range: rng,
+			Fix:   srcAnalyzer.Fix,
+		})
+		if err != nil {
+			return nil, err
+		}
+		fixes = append(fixes, source.SuggestedFixFromCommand(cmd))
+	}
 	related, err := relatedInformation(snapshot, pkg, e)
 	if err != nil {
 		return nil, err
 	}
-	return []*source.Diagnostic{{
+	diag := &source.Diagnostic{
 		URI:            spn.URI(),
 		Range:          rng,
 		Severity:       protocol.SeverityWarning,
@@ -164,7 +185,29 @@
 		Message:        e.Message,
 		Related:        related,
 		SuggestedFixes: fixes,
-	}}, nil
+	}
+	// If the fixes only delete code, assume that the diagnostic is reporting dead code.
+	if onlyDeletions(fixes) {
+		diag.Tags = []protocol.DiagnosticTag{protocol.Unnecessary}
+	}
+	return []*source.Diagnostic{diag}, nil
+}
+
+// onlyDeletions returns true if all of the suggested fixes are deletions.
+func onlyDeletions(fixes []source.SuggestedFix) bool {
+	for _, fix := range fixes {
+		for _, edits := range fix.Edits {
+			for _, edit := range edits {
+				if edit.NewText != "" {
+					return false
+				}
+				if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 {
+					return false
+				}
+			}
+		}
+	}
+	return len(fixes) > 0
 }
 
 func typesCodeHref(snapshot *snapshot, code typesinternal.ErrorCode) string {
diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go
index 1ac6c83..69b3e3f 100644
--- a/internal/lsp/cache/pkg.go
+++ b/internal/lsp/cache/pkg.go
@@ -86,17 +86,6 @@
 	return syntax
 }
 
-func (p *pkg) GetDiagnostics() map[span.URI][]*source.Diagnostic {
-	diags := map[span.URI][]*source.Diagnostic{}
-	for _, diag := range p.diagnostics {
-		// As of writing, source.Analyze modifies the diagnostics it receives.
-		// Shallow copy to avoid races.
-		clone := *diag
-		diags[diag.URI] = append(diags[diag.URI], &clone)
-	}
-	return diags
-}
-
 func (p *pkg) GetTypes() *types.Package {
 	return p.types
 }
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index fd9d6f9..228a713 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -218,7 +218,7 @@
 		if app.options != nil {
 			app.options(opts)
 		}
-		key := fmt.Sprintf("%s %v", app.wd, opts)
+		key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher)
 		if c := internalConnections[key]; c != nil {
 			return c, nil
 		}
@@ -278,6 +278,7 @@
 	if options != nil {
 		options(opts)
 	}
+	// If you add an additional option here, you must update the map key in connect.
 	params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{
 		ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
 	}
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 952922b..0608740 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -10,7 +10,6 @@
 	"sort"
 	"strings"
 
-	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/imports"
 	"golang.org/x/tools/internal/lsp/command"
@@ -126,11 +125,15 @@
 			return nil, err
 		}
 		if (wanted[protocol.QuickFix] || wanted[protocol.SourceFixAll]) && len(diagnostics) > 0 {
-			pkgQuickFixes, err := quickFixesForDiagnostics(ctx, snapshot, diagnostics, pkg.GetDiagnostics()[fh.URI()])
+			pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg)
 			if err != nil {
 				return nil, err
 			}
-			codeActions = append(codeActions, pkgQuickFixes...)
+			diagnosticFixes, err := quickFixesForDiagnostics(ctx, snapshot, diagnostics, pkgDiagnostics[fh.URI()])
+			if err != nil {
+				return nil, err
+			}
+			codeActions = append(codeActions, diagnosticFixes...)
 			analysisQuickFixes, highConfidenceEdits, err := analysisFixes(ctx, snapshot, pkg, diagnostics)
 			if err != nil {
 				return nil, err
@@ -302,7 +305,7 @@
 	if analyzer == nil {
 		return nil, source.Analyzer{}, false
 	}
-	analysisErrors, err := snapshot.Analyze(ctx, pkgID, analyzer.Analyzer)
+	analysisErrors, err := snapshot.Analyze(ctx, pkgID, []*source.Analyzer{analyzer})
 	if err != nil {
 		return nil, source.Analyzer{}, false
 	}
@@ -333,32 +336,19 @@
 		}
 	}()
 	if a, ok := snapshot.View().Options().DefaultAnalyzers[src]; ok {
-		return &a
+		return a
 	}
 	if a, ok := snapshot.View().Options().StaticcheckAnalyzers[src]; ok {
-		return &a
+		return a
 	}
 	if a, ok := snapshot.View().Options().ConvenienceAnalyzers[src]; ok {
-		return &a
-	}
-	// Hack: We publish diagnostics with the source "compiler" for type errors,
-	// but these analyzers have different names. Try both possibilities.
-	if a, ok := snapshot.View().Options().TypeErrorAnalyzers[src]; ok {
-		return &a
-	}
-	if src != "compiler" {
-		return nil
-	}
-	for _, a := range snapshot.View().Options().TypeErrorAnalyzers {
-		if a.FixesError(msg) {
-			return &a
-		}
+		return a
 	}
 	return nil
 }
 
 func convenienceFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
-	var analyzers []*analysis.Analyzer
+	var analyzers []*source.Analyzer
 	for _, a := range snapshot.View().Options().ConvenienceAnalyzers {
 		if !a.IsEnabled(snapshot.View()) {
 			continue
@@ -367,9 +357,9 @@
 			event.Error(ctx, "convenienceFixes", fmt.Errorf("no suggested fixes for convenience analyzer %s", a.Analyzer.Name))
 			continue
 		}
-		analyzers = append(analyzers, a.Analyzer)
+		analyzers = append(analyzers, a)
 	}
-	diagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers...)
+	diagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index f910398..0f7ba4a 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -256,14 +256,18 @@
 		return
 	}
 
-	typeCheckDiagnostics := pkg.GetDiagnostics()
+	pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg)
+	if err != nil {
+		event.Error(ctx, "warning: diagnosing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID()))
+		return
+	}
 	for _, cgf := range pkg.CompiledGoFiles() {
-		s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, typeCheckDiagnostics[cgf.URI])
+		s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, pkgDiagnostics[cgf.URI])
 	}
 	if includeAnalysis && !pkg.HasListOrParseErrors() {
-		reports, err := source.Analyze(ctx, snapshot, pkg, typeCheckDiagnostics)
+		reports, err := source.Analyze(ctx, snapshot, pkg, pkgDiagnostics)
 		if err != nil {
-			event.Error(ctx, "warning: diagnose package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID()))
+			event.Error(ctx, "warning: analyzing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID()))
 			return
 		}
 		for _, cgf := range pkg.CompiledGoFiles() {
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 2b82891..2b71029 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -521,7 +521,7 @@
 		// Hack: We assume that we only get one code action per range.
 		var cmds []string
 		for _, a := range actions {
-			cmds = append(cmds, fmt.Sprintf("%s (%s)", a.Command.Command, a.Title))
+			cmds = append(cmds, fmt.Sprintf("%s (%s)", a.Command, a.Title))
 		}
 		t.Fatalf("unexpected number of code actions, want %d, got %d: %v", expectedActions, len(actions), cmds)
 	}
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index ca636f4..0aa8caf 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -92,7 +92,11 @@
 	}
 	if err == nil {
 		for _, pkg := range wspkgs {
-			diagnostics = append(diagnostics, pkg.GetDiagnostics()[fh.URI()]...)
+			pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg)
+			if err != nil {
+				return nil, err
+			}
+			diagnostics = append(diagnostics, pkgDiagnostics[fh.URI()]...)
 		}
 	}
 
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 3e90d64..db8abd5 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -7,7 +7,6 @@
 import (
 	"context"
 
-	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/span"
 )
@@ -32,11 +31,10 @@
 	}
 	// If we don't have any list or parse errors, run analyses.
 	analyzers := pickAnalyzers(snapshot, pkg.HasTypeErrors())
-	analysisDiagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers...)
+	analysisDiagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers)
 	if err != nil {
 		return nil, err
 	}
-	analysisDiagnostics = cloneDiagnostics(analysisDiagnostics)
 
 	reports := map[span.URI][]*Diagnostic{}
 	// Report diagnostics and errors from root analyzers.
@@ -48,59 +46,22 @@
 		if isConvenienceAnalyzer(string(diag.Source)) {
 			continue
 		}
-		// This is a bit of a hack, but clients > 3.15 will be able to grey out unnecessary code.
-		// If we are deleting code as part of all of our suggested fixes, assume that this is dead code.
-		// TODO(golang/go#34508): Return these codes from the diagnostics themselves.
-		var tags []protocol.DiagnosticTag
-		if onlyDeletions(diag.SuggestedFixes) {
-			tags = append(tags, protocol.Unnecessary)
-		}
-		// Type error analyzers only alter the tags for existing type errors.
-		if _, ok := snapshot.View().Options().TypeErrorAnalyzers[string(diag.Source)]; ok {
-			existingDiagnostics := typeCheckDiagnostics[diag.URI]
-			for _, existing := range existingDiagnostics {
-				if r := protocol.CompareRange(diag.Range, existing.Range); r != 0 {
-					continue
-				}
-				if diag.Message != existing.Message {
-					continue
-				}
-				existing.Tags = append(existing.Tags, tags...)
-			}
-		} else {
-			diag.Tags = append(diag.Tags, tags...)
-			reports[diag.URI] = append(reports[diag.URI], diag)
-		}
+		reports[diag.URI] = append(reports[diag.URI], diag)
 	}
 	return reports, nil
 }
 
-// cloneDiagnostics makes a shallow copy of diagnostics so that Analyze
-// can add tags to them without affecting the cached diagnostics.
-func cloneDiagnostics(diags []*Diagnostic) []*Diagnostic {
-	result := []*Diagnostic{}
-	for _, d := range diags {
-		clone := *d
-		result = append(result, &clone)
-	}
-	return result
-}
-
-func pickAnalyzers(snapshot Snapshot, hadTypeErrors bool) []*analysis.Analyzer {
+func pickAnalyzers(snapshot Snapshot, hadTypeErrors bool) []*Analyzer {
 	// Always run convenience analyzers.
-	categories := []map[string]Analyzer{snapshot.View().Options().ConvenienceAnalyzers}
-	// If we had type errors, only run type error analyzers.
-	if hadTypeErrors {
-		categories = append(categories, snapshot.View().Options().TypeErrorAnalyzers)
-	} else {
+	categories := []map[string]*Analyzer{snapshot.View().Options().ConvenienceAnalyzers}
+	// If we had type errors, don't run any other analyzers.
+	if !hadTypeErrors {
 		categories = append(categories, snapshot.View().Options().DefaultAnalyzers, snapshot.View().Options().StaticcheckAnalyzers)
 	}
-	var analyzers []*analysis.Analyzer
-	for _, m := range categories {
-		for _, a := range m {
-			if a.IsEnabled(snapshot.View()) {
-				analyzers = append(analyzers, a.Analyzer)
-			}
+	var analyzers []*Analyzer
+	for _, cat := range categories {
+		for _, a := range cat {
+			analyzers = append(analyzers, a)
 		}
 	}
 	return analyzers
@@ -115,7 +76,10 @@
 	if err != nil {
 		return VersionedFileIdentity{}, nil, err
 	}
-	diagnostics := pkg.GetDiagnostics()
+	diagnostics, err := snapshot.DiagnosePackage(ctx, pkg)
+	if err != nil {
+		return VersionedFileIdentity{}, nil, err
+	}
 	fileDiags := diagnostics[fh.URI()]
 	if !pkg.HasListOrParseErrors() {
 		analysisDiags, err := Analyze(ctx, snapshot, pkg, diagnostics)
@@ -127,23 +91,6 @@
 	return fh.VersionedFileIdentity(), fileDiags, nil
 }
 
-// onlyDeletions returns true if all of the suggested fixes are deletions.
-func onlyDeletions(fixes []SuggestedFix) bool {
-	for _, fix := range fixes {
-		for _, edits := range fix.Edits {
-			for _, edit := range edits {
-				if edit.NewText != "" {
-					return false
-				}
-				if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 {
-					return false
-				}
-			}
-		}
-	}
-	return len(fixes) > 0
-}
-
 func isConvenienceAnalyzer(category string) bool {
 	for _, a := range DefaultOptions().ConvenienceAnalyzers {
 		if category == a.Analyzer.Name {
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 55bd86f..3f50eac 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -153,7 +153,7 @@
 				DefaultAnalyzers:     defaultAnalyzers(),
 				TypeErrorAnalyzers:   typeErrorAnalyzers(),
 				ConvenienceAnalyzers: convenienceAnalyzers(),
-				StaticcheckAnalyzers: map[string]Analyzer{},
+				StaticcheckAnalyzers: map[string]*Analyzer{},
 				GoDiff:               true,
 			},
 		}
@@ -418,10 +418,10 @@
 	ComputeEdits         diff.ComputeEdits
 	URLRegexp            *regexp.Regexp
 	GofumptFormat        func(ctx context.Context, src []byte) ([]byte, error)
-	DefaultAnalyzers     map[string]Analyzer
-	TypeErrorAnalyzers   map[string]Analyzer
-	ConvenienceAnalyzers map[string]Analyzer
-	StaticcheckAnalyzers map[string]Analyzer
+	DefaultAnalyzers     map[string]*Analyzer
+	TypeErrorAnalyzers   map[string]*Analyzer
+	ConvenienceAnalyzers map[string]*Analyzer
+	StaticcheckAnalyzers map[string]*Analyzer
 }
 
 // InternalOptions contains settings that are not intended for use by the
@@ -656,8 +656,8 @@
 	result.BuildFlags = copySlice(o.BuildFlags)
 	result.DirectoryFilters = copySlice(o.DirectoryFilters)
 
-	copyAnalyzerMap := func(src map[string]Analyzer) map[string]Analyzer {
-		dst := make(map[string]Analyzer)
+	copyAnalyzerMap := func(src map[string]*Analyzer) map[string]*Analyzer {
+		dst := make(map[string]*Analyzer)
 		for k, v := range src {
 			dst[k] = v
 		}
@@ -671,7 +671,7 @@
 }
 
 func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer) {
-	o.StaticcheckAnalyzers[a.Name] = Analyzer{Analyzer: a, Enabled: true}
+	o.StaticcheckAnalyzers[a.Name] = &Analyzer{Analyzer: a, Enabled: true}
 }
 
 // enableAllExperiments turns on all of the experimental "off-by-default"
@@ -1050,7 +1050,7 @@
 
 // EnabledAnalyzers returns all of the analyzers enabled for the given
 // snapshot.
-func EnabledAnalyzers(snapshot Snapshot) (analyzers []Analyzer) {
+func EnabledAnalyzers(snapshot Snapshot) (analyzers []*Analyzer) {
 	for _, a := range snapshot.View().Options().DefaultAnalyzers {
 		if a.IsEnabled(snapshot.View()) {
 			analyzers = append(analyzers, a)
@@ -1074,35 +1074,31 @@
 	return analyzers
 }
 
-func typeErrorAnalyzers() map[string]Analyzer {
-	return map[string]Analyzer{
+func typeErrorAnalyzers() map[string]*Analyzer {
+	return map[string]*Analyzer{
 		fillreturns.Analyzer.Name: {
 			Analyzer:       fillreturns.Analyzer,
-			FixesError:     fillreturns.FixesError,
 			HighConfidence: true,
 			Enabled:        true,
 		},
 		nonewvars.Analyzer.Name: {
-			Analyzer:   nonewvars.Analyzer,
-			FixesError: nonewvars.FixesError,
-			Enabled:    true,
+			Analyzer: nonewvars.Analyzer,
+			Enabled:  true,
 		},
 		noresultvalues.Analyzer.Name: {
-			Analyzer:   noresultvalues.Analyzer,
-			FixesError: noresultvalues.FixesError,
-			Enabled:    true,
+			Analyzer: noresultvalues.Analyzer,
+			Enabled:  true,
 		},
 		undeclaredname.Analyzer.Name: {
-			Analyzer:   undeclaredname.Analyzer,
-			FixesError: undeclaredname.FixesError,
-			Fix:        UndeclaredName,
-			Enabled:    true,
+			Analyzer: undeclaredname.Analyzer,
+			Fix:      UndeclaredName,
+			Enabled:  true,
 		},
 	}
 }
 
-func convenienceAnalyzers() map[string]Analyzer {
-	return map[string]Analyzer{
+func convenienceAnalyzers() map[string]*Analyzer {
+	return map[string]*Analyzer{
 		fillstruct.Analyzer.Name: {
 			Analyzer: fillstruct.Analyzer,
 			Fix:      FillStruct,
@@ -1111,8 +1107,8 @@
 	}
 }
 
-func defaultAnalyzers() map[string]Analyzer {
-	return map[string]Analyzer{
+func defaultAnalyzers() map[string]*Analyzer {
+	return map[string]*Analyzer{
 		// The traditional vet suite:
 		asmdecl.Analyzer.Name:       {Analyzer: asmdecl.Analyzer, Enabled: true},
 		assign.Analyzer.Name:        {Analyzer: assign.Analyzer, Enabled: true},
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index a0e5252..d36c41b 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -84,8 +84,12 @@
 	// string for the objects.
 	PosToDecl(ctx context.Context, pgf *ParsedGoFile) (map[token.Pos]ast.Decl, error)
 
+	// DiagnosePackage returns basic diagnostics, including list, parse, and type errors
+	// for pkg, grouped by file.
+	DiagnosePackage(ctx context.Context, pkg Package) (map[span.URI][]*Diagnostic, error)
+
 	// Analyze runs the analyses for the given package at this snapshot.
-	Analyze(ctx context.Context, pkgID string, analyzers ...*analysis.Analyzer) ([]*Diagnostic, error)
+	Analyze(ctx context.Context, pkgID string, analyzers []*Analyzer) ([]*Diagnostic, error)
 
 	// RunGoCommandPiped runs the given `go` command, writing its output
 	// to stdout and stderr. Verb, Args, and WorkingDir must be specified.
@@ -519,11 +523,6 @@
 	// If this is true, then we can apply the suggested fixes
 	// as part of a source.FixAll codeaction.
 	HighConfidence bool
-
-	// FixesError is only set for type-error analyzers.
-	// It reports true if the message provided indicates an error that could be
-	// fixed by the analyzer.
-	FixesError func(msg string) bool
 }
 
 func (a Analyzer) IsEnabled(view View) bool {
@@ -548,7 +547,6 @@
 	CompiledGoFiles() []*ParsedGoFile
 	File(uri span.URI) (*ParsedGoFile, error)
 	GetSyntax() []*ast.File
-	GetDiagnostics() map[span.URI][]*Diagnostic
 	GetTypes() *types.Package
 	GetTypesInfo() *types.Info
 	GetTypesSizes() types.Sizes
@@ -587,8 +585,8 @@
 	Tags    []protocol.DiagnosticTag
 	Related []RelatedInformation
 
-	// SuggestedFixes is used to generate quick fixes for a CodeAction request.
-	// It isn't part of the Diagnostic type.
+	// Fields below are used internally to generate quick fixes. They aren't
+	// part of the LSP spec and don't leave the server.
 	SuggestedFixes []SuggestedFix
 }