internal/lsp: use pre-existing quick fixes for analysis diagnostics
Now that we're generating quick fixes at analysis time, we can use those
in code action requests and delete a fair amount of redundancy. The
codeAction function is a little cluttered, but I want to get it all in
one place before I decide how to split it up.
Change-Id: Icd91e2547542cce0a05c18c02a088833f71232a4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/297532
Trust: Heschi Kreinick <heschi@google.com>
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/internal/lsp/cache/errors.go b/internal/lsp/cache/errors.go
index 072d78f..a876473 100644
--- a/internal/lsp/cache/errors.go
+++ b/internal/lsp/cache/errors.go
@@ -185,6 +185,7 @@
Message: e.Message,
Related: related,
SuggestedFixes: fixes,
+ Analyzer: srcAnalyzer,
}
// If the fixes only delete code, assume that the diagnostic is reporting dead code.
if onlyDeletions(fixes) {
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 0608740..69ee199 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -62,14 +62,18 @@
switch fh.Kind() {
case source.Mod:
if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
- modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics)
+ diags, err := mod.DiagnosticsForMod(ctx, snapshot, fh)
if source.IsNonFatalGoModError(err) {
return nil, nil
}
if err != nil {
return nil, err
}
- codeActions = append(codeActions, modQuickFixes...)
+ quickFixes, err := quickFixesForDiagnostics(ctx, snapshot, diagnostics, diags)
+ if err != nil {
+ return nil, err
+ }
+ codeActions = append(codeActions, quickFixes...)
}
case source.Go:
// Don't suggest fixes for generated files, since they are generally
@@ -125,40 +129,61 @@
return nil, err
}
if (wanted[protocol.QuickFix] || wanted[protocol.SourceFixAll]) && len(diagnostics) > 0 {
- pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg)
+ analysisDiags, err := source.Analyze(ctx, snapshot, pkg)
if err != nil {
return nil, err
}
- 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
- }
- if wanted[protocol.QuickFix] {
- // Add the quick fixes reported by go/analysis.
- codeActions = append(codeActions, analysisQuickFixes...)
- // If there are any diagnostics relating to the go.mod file,
- // add their corresponding quick fixes.
- modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics)
- if source.IsNonFatalGoModError(err) {
- // Not a fatal error.
- event.Error(ctx, "module suggested fixes failed", err, tag.Directory.Of(snapshot.View().Folder()))
+ if wanted[protocol.QuickFix] {
+ pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg)
+ if err != nil {
+ return nil, err
}
- codeActions = append(codeActions, modQuickFixes...)
+ quickFixDiags := append(pkgDiagnostics[uri], analysisDiags[uri]...)
+ modURI := snapshot.GoModForFile(fh.URI())
+ if modURI != "" {
+ modFH, err := snapshot.GetVersionedFile(ctx, modURI)
+ if err != nil {
+ return nil, err
+ }
+ modDiags, err := mod.DiagnosticsForMod(ctx, snapshot, modFH)
+ if err != nil && !source.IsNonFatalGoModError(err) {
+ // Not a fatal error.
+ event.Error(ctx, "module suggested fixes failed", err, tag.Directory.Of(snapshot.View().Folder()))
+ }
+ quickFixDiags = append(quickFixDiags, modDiags...)
+ }
+ quickFixes, err := quickFixesForDiagnostics(ctx, snapshot, diagnostics, quickFixDiags)
+ if err != nil {
+ return nil, err
+ }
+ codeActions = append(codeActions, quickFixes...)
+
}
- if wanted[protocol.SourceFixAll] && len(highConfidenceEdits) > 0 {
- codeActions = append(codeActions, protocol.CodeAction{
- Title: "Simplifications",
- Kind: protocol.SourceFixAll,
- Edit: protocol.WorkspaceEdit{
- DocumentChanges: highConfidenceEdits,
- },
- })
+ if wanted[protocol.SourceFixAll] {
+ var fixAllEdits []protocol.TextDocumentEdit
+ for _, ad := range analysisDiags[uri] {
+ if ad.Analyzer == nil || !ad.Analyzer.HighConfidence {
+ continue
+ }
+ for _, fix := range ad.SuggestedFixes {
+ edits := fix.Edits[fh.URI()]
+ if len(edits) == 0 {
+ continue
+ }
+ fixAllEdits = append(fixAllEdits, documentChanges(fh, edits)...)
+ }
+
+ }
+ if len(fixAllEdits) != 0 {
+ codeActions = append(codeActions, protocol.CodeAction{
+ Title: "Simplifications",
+ Kind: protocol.SourceFixAll,
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: fixAllEdits,
+ },
+ })
+ }
}
}
if ctx.Err() != nil {
@@ -253,78 +278,6 @@
return results
}
-func analysisFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, []protocol.TextDocumentEdit, error) {
- if len(diagnostics) == 0 {
- return nil, nil, nil
- }
- var (
- codeActions []protocol.CodeAction
- sourceFixAllEdits []protocol.TextDocumentEdit
- )
- for _, diag := range diagnostics {
- srcErr, analyzer, ok := findDiagnostic(ctx, snapshot, pkg.ID(), diag)
- if !ok {
- continue
- }
- // If the suggested fix for the diagnostic is expected to be separate,
- // see if there are any supported commands available.
- if analyzer.Fix != "" {
- action, err := diagnosticToCommandCodeAction(ctx, snapshot, srcErr, &diag, protocol.QuickFix)
- if err != nil {
- return nil, nil, err
- }
- codeActions = append(codeActions, *action)
- continue
- }
- for _, fix := range srcErr.SuggestedFixes {
- action := protocol.CodeAction{
- Title: fix.Title,
- Kind: protocol.QuickFix,
- Diagnostics: []protocol.Diagnostic{diag},
- Edit: protocol.WorkspaceEdit{},
- }
- for uri, edits := range fix.Edits {
- fh, err := snapshot.GetVersionedFile(ctx, uri)
- if err != nil {
- return nil, nil, err
- }
- docChanges := documentChanges(fh, edits)
- if analyzer.HighConfidence {
- sourceFixAllEdits = append(sourceFixAllEdits, docChanges...)
- }
- action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, docChanges...)
- }
- codeActions = append(codeActions, action)
- }
- }
- return codeActions, sourceFixAllEdits, nil
-}
-
-func findDiagnostic(ctx context.Context, snapshot source.Snapshot, pkgID string, diag protocol.Diagnostic) (*source.Diagnostic, source.Analyzer, bool) {
- analyzer := diagnosticToAnalyzer(snapshot, diag.Source, diag.Message)
- if analyzer == nil {
- return nil, source.Analyzer{}, false
- }
- analysisErrors, err := snapshot.Analyze(ctx, pkgID, []*source.Analyzer{analyzer})
- if err != nil {
- return nil, source.Analyzer{}, false
- }
- for _, err := range analysisErrors {
- if err.Message != diag.Message {
- continue
- }
- if protocol.CompareRange(err.Range, diag.Range) != 0 {
- continue
- }
- if string(err.Source) != analyzer.Analyzer.Name {
- continue
- }
- // The error matches.
- return err, *analyzer, true
- }
- return nil, source.Analyzer{}, false
-}
-
// diagnosticToAnalyzer return the analyzer associated with a given diagnostic.
// It assumes that the diagnostic's source will be the name of the analyzer.
// If this changes, this approach will need to be reworked.
@@ -478,29 +431,6 @@
}
}
-func moduleQuickFixes(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, pdiags []protocol.Diagnostic) ([]protocol.CodeAction, error) {
- var modFH source.VersionedFileHandle
- switch fh.Kind() {
- case source.Mod:
- modFH = fh
- case source.Go:
- modURI := snapshot.GoModForFile(fh.URI())
- if modURI == "" {
- return nil, nil
- }
- var err error
- modFH, err = snapshot.GetVersionedFile(ctx, modURI)
- if err != nil {
- return nil, err
- }
- }
- diags, err := mod.DiagnosticsForMod(ctx, snapshot, modFH)
- if err != nil {
- return nil, err
- }
- return quickFixesForDiagnostics(ctx, snapshot, pdiags, diags)
-}
-
func quickFixesForDiagnostics(ctx context.Context, snapshot source.Snapshot, pdiags []protocol.Diagnostic, sdiags []*source.Diagnostic) ([]protocol.CodeAction, error) {
var quickFixes []protocol.CodeAction
for _, e := range sdiags {
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 0f7ba4a..0368771 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -265,7 +265,7 @@
s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, pkgDiagnostics[cgf.URI])
}
if includeAnalysis && !pkg.HasListOrParseErrors() {
- reports, err := source.Analyze(ctx, snapshot, pkg, pkgDiagnostics)
+ reports, err := source.Analyze(ctx, snapshot, pkg)
if err != nil {
event.Error(ctx, "warning: analyzing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID()))
return
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index db8abd5..e1a7416 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -23,7 +23,7 @@
Message string
}
-func Analyze(ctx context.Context, snapshot Snapshot, pkg Package, typeCheckDiagnostics map[span.URI][]*Diagnostic) (map[span.URI][]*Diagnostic, error) {
+func Analyze(ctx context.Context, snapshot Snapshot, pkg Package) (map[span.URI][]*Diagnostic, error) {
// Exit early if the context has been canceled. This also protects us
// from a race on Options, see golang/go#36699.
if ctx.Err() != nil {
@@ -82,7 +82,7 @@
}
fileDiags := diagnostics[fh.URI()]
if !pkg.HasListOrParseErrors() {
- analysisDiags, err := Analyze(ctx, snapshot, pkg, diagnostics)
+ analysisDiags, err := Analyze(ctx, snapshot, pkg)
if err != nil {
return VersionedFileIdentity{}, nil, err
}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index d36c41b..57712b6 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -588,6 +588,7 @@
// 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
+ Analyzer *Analyzer
}
type DiagnosticSource string