internal/lsp: refactor and propagate errors from code actions
This change makes sure that all errors in code actions are returned,
instead of ignored. There is also a refactoring of the suggested fixes
for go.mod files so that we don't compute them unless there is already a
matching diagnostic.
Change-Id: I34afda0116f3cc7e47809d980a0171487787e55e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/221221
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rohan Challa <rohan@golang.org>
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 51dac8a..e125375 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -7,15 +7,14 @@
import (
"context"
"fmt"
+ "regexp"
"sort"
"strings"
"golang.org/x/tools/internal/imports"
- "golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/mod"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/telemetry/event"
)
func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
@@ -52,7 +51,7 @@
if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
codeActions = append(codeActions, mod.SuggestedFixes(ctx, snapshot, fh, diagnostics)...)
}
- if !wanted[protocol.SourceOrganizeImports] {
+ if wanted[protocol.SourceOrganizeImports] {
codeActions = append(codeActions, protocol.CodeAction{
Title: "Tidy",
Kind: protocol.SourceOrganizeImports,
@@ -66,75 +65,74 @@
case source.Go:
diagnostics := params.Context.Diagnostics
- var importEdits []protocol.TextEdit
- var importEditsPerFix []*source.ImportFix
- var analysisQuickFixes []protocol.CodeAction
- var highConfidenceEdits []protocol.TextDocumentEdit
-
- // Retrieve any necessary import edits or fixes.
- if wanted[protocol.QuickFix] && len(diagnostics) > 0 || wanted[protocol.SourceOrganizeImports] {
- importEdits, importEditsPerFix, err = source.AllImportsFixes(ctx, snapshot, fh)
+ // First, process any missing imports and pair them with the
+ // diagnostics they fix.
+ if wantQuickFixes := wanted[protocol.QuickFix] && len(diagnostics) > 0; wantQuickFixes || wanted[protocol.SourceOrganizeImports] {
+ importEdits, importEditsPerFix, err := source.AllImportsFixes(ctx, snapshot, fh)
if err != nil {
return nil, err
}
+ // Separate this into a set of codeActions per diagnostic, where
+ // each action is the addition, removal, or renaming of one import.
+ if wantQuickFixes {
+ for _, importFix := range importEditsPerFix {
+ fixes := importDiagnostics(importFix.Fix, diagnostics)
+ if len(fixes) == 0 {
+ continue
+ }
+ codeActions = append(codeActions, protocol.CodeAction{
+ Title: importFixTitle(importFix.Fix),
+ Kind: protocol.QuickFix,
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: documentChanges(fh, importFix.Edits),
+ },
+ Diagnostics: fixes,
+ })
+ }
+ }
+ // Send all of the import edits as one code action if the file is
+ // being organized.
+ if wanted[protocol.SourceOrganizeImports] && len(importEdits) > 0 {
+ codeActions = append(codeActions, protocol.CodeAction{
+ Title: "Organize Imports",
+ Kind: protocol.SourceOrganizeImports,
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: documentChanges(fh, importEdits),
+ },
+ })
+ }
+ }
+ // Check for context cancellation before processing analysis fixes.
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
}
// Retrieve any necessary analysis fixes or edits.
if (wanted[protocol.QuickFix] || wanted[protocol.SourceFixAll]) && len(diagnostics) > 0 {
- analysisQuickFixes, highConfidenceEdits, err = analysisFixes(ctx, snapshot, fh, diagnostics)
+ analysisQuickFixes, highConfidenceEdits, err := analysisFixes(ctx, snapshot, fh, diagnostics)
if err != nil {
- event.Error(ctx, "analysis fixes failed", err, tag.URI.Of(uri))
+ return nil, err
}
- }
+ if wanted[protocol.QuickFix] {
+ // Add the quick fixes reported by go/analysis.
+ codeActions = append(codeActions, analysisQuickFixes...)
- if wanted[protocol.QuickFix] && len(diagnostics) > 0 {
- // First, add the quick fixes reported by go/analysis.
- codeActions = append(codeActions, analysisQuickFixes...)
-
- // If we also have diagnostics for missing imports, we can associate them with quick fixes.
- if findImportErrors(diagnostics) {
- // Separate this into a set of codeActions per diagnostic, where
- // each action is the addition, removal, or renaming of one import.
- for _, importFix := range importEditsPerFix {
- // Get the diagnostics this fix would affect.
- if fixDiagnostics := importDiagnostics(importFix.Fix, diagnostics); len(fixDiagnostics) > 0 {
- codeActions = append(codeActions, protocol.CodeAction{
- Title: importFixTitle(importFix.Fix),
- Kind: protocol.QuickFix,
- Edit: protocol.WorkspaceEdit{
- DocumentChanges: documentChanges(fh, importFix.Edits),
- },
- Diagnostics: fixDiagnostics,
- })
- }
+ // If there are any diagnostics relating to the go.mod file,
+ // add their corresponding quick fixes.
+ moduleQuickFixes, err := getModuleQuickFixes(ctx, snapshot, diagnostics)
+ if err != nil {
+ return nil, err
}
+ codeActions = append(codeActions, moduleQuickFixes...)
}
-
- // Get any actions that might be attributed to missing modules in the go.mod file.
- actions, err := mod.SuggestedGoFixes(ctx, snapshot, diagnostics)
- if err != nil {
- event.Error(ctx, "quick fixes failed", err, tag.URI.Of(uri))
+ if wanted[protocol.SourceFixAll] && len(highConfidenceEdits) > 0 {
+ codeActions = append(codeActions, protocol.CodeAction{
+ Title: "Simplifications",
+ Kind: protocol.SourceFixAll,
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: highConfidenceEdits,
+ },
+ })
}
- if len(actions) > 0 {
- codeActions = append(codeActions, actions...)
- }
- }
- if wanted[protocol.SourceOrganizeImports] && len(importEdits) > 0 {
- codeActions = append(codeActions, protocol.CodeAction{
- Title: "Organize Imports",
- Kind: protocol.SourceOrganizeImports,
- Edit: protocol.WorkspaceEdit{
- DocumentChanges: documentChanges(fh, importEdits),
- },
- })
- }
- if wanted[protocol.SourceFixAll] && len(highConfidenceEdits) > 0 {
- codeActions = append(codeActions, protocol.CodeAction{
- Title: "Simplifications",
- Kind: protocol.SourceFixAll,
- Edit: protocol.WorkspaceEdit{
- DocumentChanges: highConfidenceEdits,
- },
- })
}
default:
// Unsupported file kind for a code action.
@@ -143,6 +141,47 @@
return codeActions, nil
}
+var missingRequirementRe = regexp.MustCompile(`(.+) is not in your go.mod file`)
+
+func getModuleQuickFixes(ctx context.Context, snapshot source.Snapshot, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
+ // Don't bother getting quick fixes if we have no relevant diagnostics.
+ var missingDeps map[string]protocol.Diagnostic
+ for _, diagnostic := range diagnostics {
+ matches := missingRequirementRe.FindStringSubmatch(diagnostic.Message)
+ if len(matches) != 2 {
+ continue
+ }
+ if missingDeps == nil {
+ missingDeps = make(map[string]protocol.Diagnostic)
+ }
+ missingDeps[matches[1]] = diagnostic
+ }
+ if len(missingDeps) == 0 {
+ return nil, nil
+ }
+ // Get suggested fixes for each missing dependency.
+ edits, err := mod.SuggestedGoFixes(ctx, snapshot)
+ if err != nil {
+ return nil, err
+ }
+ var codeActions []protocol.CodeAction
+ for dep, diagnostic := range missingDeps {
+ edit, ok := edits[dep]
+ if !ok {
+ continue
+ }
+ codeActions = append(codeActions, protocol.CodeAction{
+ Title: fmt.Sprintf("Add %s to go.mod", dep),
+ Diagnostics: []protocol.Diagnostic{diagnostic},
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: []protocol.TextDocumentEdit{edit},
+ },
+ Kind: protocol.QuickFix,
+ })
+ }
+ return codeActions, nil
+}
+
func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
for _, kinds := range s.session.Options().SupportedCodeActions {
@@ -160,28 +199,6 @@
return result
}
-// findImports determines if a given diagnostic represents an error that could
-// be fixed by organizing imports.
-// TODO(rstambler): We need a better way to check this than string matching.
-func findImportErrors(diagnostics []protocol.Diagnostic) bool {
- for _, diagnostic := range diagnostics {
- // "undeclared name: X" may be an unresolved import.
- if strings.HasPrefix(diagnostic.Message, "undeclared name: ") {
- return true
- }
- // "could not import: X" may be an invalid import.
- if strings.HasPrefix(diagnostic.Message, "could not import: ") {
- return true
- }
- // "X imported but not used" is an unused import.
- // "X imported but not used as Y" is an unused import.
- if strings.Contains(diagnostic.Message, " imported but not used") {
- return true
- }
- }
- return false
-}
-
func importFixTitle(fix *imports.ImportFix) string {
var str string
switch fix.FixType {
@@ -258,8 +275,7 @@
for uri, edits := range fix.Edits {
fh, err := snapshot.GetFile(uri)
if err != nil {
- event.Error(ctx, "no file", err, tag.URI.Of(uri))
- continue
+ return nil, nil, err
}
docChanges := documentChanges(fh, edits)
if analyzer.HighConfidence {
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index a4c62ac..e36db73 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -8,8 +8,6 @@
import (
"context"
- "fmt"
- "regexp"
"golang.org/x/mod/modfile"
"golang.org/x/tools/internal/lsp/debug/tag"
@@ -115,11 +113,10 @@
return actions
}
-func SuggestedGoFixes(ctx context.Context, snapshot source.Snapshot, diags []protocol.Diagnostic) ([]protocol.CodeAction, error) {
- // TODO: We will want to support diagnostics for go.mod files even when the -modfile flag is turned off.
+func SuggestedGoFixes(ctx context.Context, snapshot source.Snapshot) (map[string]protocol.TextDocumentEdit, error) {
+ // TODO(rstambler): Support diagnostics for go.mod files even when the
+ // -modfile flag is turned off.
realURI, tempURI := snapshot.View().ModFiles()
-
- // Check the case when the tempModfile flag is turned off.
if realURI == "" || tempURI == "" {
return nil, nil
}
@@ -139,23 +136,16 @@
if err != nil {
return nil, err
}
+ if len(missingDeps) == 0 {
+ return nil, nil
+ }
// Get the contents of the go.mod file before we make any changes.
oldContents, _, err := realfh.Read(ctx)
if err != nil {
return nil, err
}
-
- var actions []protocol.CodeAction
- for _, diag := range diags {
- re := regexp.MustCompile(`(.+) is not in your go.mod file`)
- matches := re.FindStringSubmatch(diag.Message)
- if len(matches) != 2 {
- continue
- }
- req := missingDeps[matches[1]]
- if req == nil {
- continue
- }
+ 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 {
return nil, err
@@ -175,27 +165,17 @@
if err != nil {
return nil, err
}
- action := protocol.CodeAction{
- Title: fmt.Sprintf("Add %s to go.mod", req.Mod.Path),
- Kind: protocol.QuickFix,
- Diagnostics: []protocol.Diagnostic{diag},
- Edit: protocol.WorkspaceEdit{
- DocumentChanges: []protocol.TextDocumentEdit{
- {
- TextDocument: protocol.VersionedTextDocumentIdentifier{
- Version: realfh.Identity().Version,
- TextDocumentIdentifier: protocol.TextDocumentIdentifier{
- URI: protocol.URIFromSpanURI(realfh.Identity().URI),
- },
- },
- Edits: edits,
- },
+ textDocumentEdits[dep] = protocol.TextDocumentEdit{
+ TextDocument: protocol.VersionedTextDocumentIdentifier{
+ Version: realfh.Identity().Version,
+ TextDocumentIdentifier: protocol.TextDocumentIdentifier{
+ URI: protocol.URIFromSpanURI(realfh.Identity().URI),
},
},
+ Edits: edits,
}
- actions = append(actions, action)
}
- return actions, nil
+ return textDocumentEdits, nil
}
func sameDiagnostic(d protocol.Diagnostic, e source.Error) bool {
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index d55ed28..06d0f7b 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -81,14 +81,12 @@
defer done()
pgh := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull)
- err = snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+ if err := snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
allFixEdits, editsPerFix, err = computeImportEdits(ctx, snapshot.View(), pgh, opts)
return err
- })
- if err != nil {
+ }); err != nil {
return nil, nil, errors.Errorf("computing fix edits: %v", err)
}
-
return allFixEdits, editsPerFix, nil
}