Skip to content

Commit 11e8f6b

Browse files
committed
internal/lsp: refactor codeAction
As much as possible, try to unify the codeAction code paths. We always run analysis now. And rather than assuming certain categories of analyzers will generate certain kinds of code actions, mark them explicitly and use that information to filter the actions afterward. Change-Id: I8154cd67aa8b59b2a6c8aa9c3ea811de2e190db4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/300170 Trust: Heschi Kreinick <[email protected]> Run-TryBot: Heschi Kreinick <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent 1523bb4 commit 11e8f6b

File tree

6 files changed

+139
-217
lines changed

6 files changed

+139
-217
lines changed

gopls/internal/regtest/watch/watch_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ func _() {
370370
// Tests golang/go#38498. Delete a file and then force a reload.
371371
// Assert that we no longer try to load the file.
372372
func TestDeleteFiles(t *testing.T) {
373+
testenv.NeedsGo1Point(t, 13) // Poor overlay support causes problems on 1.12.
373374
const pkg = `
374375
-- go.mod --
375376
module mod.com

internal/lsp/code_action.go

Lines changed: 105 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
6969
if err != nil {
7070
return nil, err
7171
}
72-
quickFixes, err := quickFixesForDiagnostics(ctx, snapshot, diagnostics, diags)
72+
quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, diags)
7373
if err != nil {
7474
return nil, err
7575
}
@@ -128,75 +128,68 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
128128
if err != nil {
129129
return nil, err
130130
}
131-
if (wanted[protocol.QuickFix] || wanted[protocol.SourceFixAll]) && len(diagnostics) > 0 {
132-
analysisDiags, err := source.Analyze(ctx, snapshot, pkg)
131+
132+
pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg)
133+
if err != nil {
134+
return nil, err
135+
}
136+
analysisDiags, err := source.Analyze(ctx, snapshot, pkg, true)
137+
if err != nil {
138+
return nil, err
139+
}
140+
fileDiags := append(pkgDiagnostics[uri], analysisDiags[uri]...)
141+
modURI := snapshot.GoModForFile(fh.URI())
142+
if modURI != "" {
143+
modFH, err := snapshot.GetVersionedFile(ctx, modURI)
133144
if err != nil {
134145
return nil, err
135146
}
136-
137-
if wanted[protocol.QuickFix] {
138-
pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg)
139-
if err != nil {
140-
return nil, err
141-
}
142-
quickFixDiags := append(pkgDiagnostics[uri], analysisDiags[uri]...)
143-
modURI := snapshot.GoModForFile(fh.URI())
144-
if modURI != "" {
145-
modFH, err := snapshot.GetVersionedFile(ctx, modURI)
146-
if err != nil {
147-
return nil, err
148-
}
149-
modDiags, err := mod.DiagnosticsForMod(ctx, snapshot, modFH)
150-
if err != nil && !source.IsNonFatalGoModError(err) {
151-
// Not a fatal error.
152-
event.Error(ctx, "module suggested fixes failed", err, tag.Directory.Of(snapshot.View().Folder()))
153-
}
154-
quickFixDiags = append(quickFixDiags, modDiags...)
155-
}
156-
quickFixes, err := quickFixesForDiagnostics(ctx, snapshot, diagnostics, quickFixDiags)
157-
if err != nil {
158-
return nil, err
159-
}
160-
codeActions = append(codeActions, quickFixes...)
161-
147+
modDiags, err := mod.DiagnosticsForMod(ctx, snapshot, modFH)
148+
if err != nil && !source.IsNonFatalGoModError(err) {
149+
// Not a fatal error.
150+
event.Error(ctx, "module suggested fixes failed", err, tag.Directory.Of(snapshot.View().Folder()))
162151
}
163-
if wanted[protocol.SourceFixAll] {
164-
var fixAllEdits []protocol.TextDocumentEdit
165-
for _, ad := range analysisDiags[uri] {
166-
if ad.Analyzer == nil || !ad.Analyzer.HighConfidence {
167-
continue
168-
}
169-
for _, fix := range ad.SuggestedFixes {
170-
edits := fix.Edits[fh.URI()]
171-
if len(edits) == 0 {
172-
continue
173-
}
174-
fixAllEdits = append(fixAllEdits, documentChanges(fh, edits)...)
175-
}
152+
fileDiags = append(fileDiags, modDiags...)
153+
}
176154

177-
}
178-
if len(fixAllEdits) != 0 {
179-
codeActions = append(codeActions, protocol.CodeAction{
180-
Title: "Simplifications",
181-
Kind: protocol.SourceFixAll,
182-
Edit: protocol.WorkspaceEdit{
183-
DocumentChanges: fixAllEdits,
184-
},
185-
})
186-
}
155+
// Split diagnostics into fixes, which must match incoming diagnostics,
156+
// and non-fixes, which must match the requested range. Build actions
157+
// for all of them.
158+
var fixDiags, nonFixDiags []*source.Diagnostic
159+
for _, d := range fileDiags {
160+
if len(d.SuggestedFixes) == 0 {
161+
continue
162+
}
163+
kind := protocol.QuickFix
164+
if d.Analyzer != nil && d.Analyzer.ActionKind != "" {
165+
kind = d.Analyzer.ActionKind
166+
}
167+
if kind == protocol.QuickFix || kind == protocol.SourceFixAll {
168+
fixDiags = append(fixDiags, d)
169+
} else {
170+
nonFixDiags = append(nonFixDiags, d)
187171
}
188172
}
189-
if ctx.Err() != nil {
190-
return nil, ctx.Err()
173+
174+
fixActions, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, fixDiags)
175+
if err != nil {
176+
return nil, err
191177
}
192-
// Add any suggestions that do not necessarily fix any diagnostics.
193-
if wanted[protocol.RefactorRewrite] {
194-
fixes, err := convenienceFixes(ctx, snapshot, pkg, uri, params.Range)
178+
codeActions = append(codeActions, fixActions...)
179+
180+
for _, nonfix := range nonFixDiags {
181+
// For now, only show diagnostics for matching lines. Maybe we should
182+
// alter this behavior in the future, depending on the user experience.
183+
if !protocol.Intersect(nonfix.Range, params.Range) {
184+
continue
185+
}
186+
actions, err := codeActionsForDiagnostic(ctx, snapshot, nonfix, nil)
195187
if err != nil {
196188
return nil, err
197189
}
198-
codeActions = append(codeActions, fixes...)
190+
codeActions = append(codeActions, actions...)
199191
}
192+
200193
if wanted[protocol.RefactorExtract] {
201194
fixes, err := extractionFixes(ctx, snapshot, pkg, uri, params.Range)
202195
if err != nil {
@@ -217,7 +210,14 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
217210
// Unsupported file kind for a code action.
218211
return nil, nil
219212
}
220-
return codeActions, nil
213+
214+
var filtered []protocol.CodeAction
215+
for _, action := range codeActions {
216+
if wanted[action.Kind] {
217+
filtered = append(filtered, action)
218+
}
219+
}
220+
return filtered, nil
221221
}
222222

223223
func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
@@ -278,94 +278,6 @@ func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic
278278
return results
279279
}
280280

281-
// diagnosticToAnalyzer return the analyzer associated with a given diagnostic.
282-
// It assumes that the diagnostic's source will be the name of the analyzer.
283-
// If this changes, this approach will need to be reworked.
284-
func diagnosticToAnalyzer(snapshot source.Snapshot, src, msg string) (analyzer *source.Analyzer) {
285-
// Make sure that the analyzer we found is enabled.
286-
defer func() {
287-
if analyzer != nil && !analyzer.IsEnabled(snapshot.View()) {
288-
analyzer = nil
289-
}
290-
}()
291-
if a, ok := snapshot.View().Options().DefaultAnalyzers[src]; ok {
292-
return a
293-
}
294-
if a, ok := snapshot.View().Options().StaticcheckAnalyzers[src]; ok {
295-
return a
296-
}
297-
if a, ok := snapshot.View().Options().ConvenienceAnalyzers[src]; ok {
298-
return a
299-
}
300-
return nil
301-
}
302-
303-
func convenienceFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
304-
var analyzers []*source.Analyzer
305-
for _, a := range snapshot.View().Options().ConvenienceAnalyzers {
306-
if !a.IsEnabled(snapshot.View()) {
307-
continue
308-
}
309-
if a.Fix == "" {
310-
event.Error(ctx, "convenienceFixes", fmt.Errorf("no suggested fixes for convenience analyzer %s", a.Analyzer.Name))
311-
continue
312-
}
313-
analyzers = append(analyzers, a)
314-
}
315-
diagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers)
316-
if err != nil {
317-
return nil, err
318-
}
319-
var codeActions []protocol.CodeAction
320-
for _, d := range diagnostics {
321-
// For now, only show diagnostics for matching lines. Maybe we should
322-
// alter this behavior in the future, depending on the user experience.
323-
if d.URI != uri {
324-
continue
325-
}
326-
327-
if !protocol.Intersect(d.Range, rng) {
328-
continue
329-
}
330-
action, err := diagnosticToCommandCodeAction(ctx, snapshot, d, nil, protocol.RefactorRewrite)
331-
if err != nil {
332-
return nil, err
333-
}
334-
codeActions = append(codeActions, *action)
335-
}
336-
return codeActions, nil
337-
}
338-
339-
func diagnosticToCommandCodeAction(ctx context.Context, snapshot source.Snapshot, sd *source.Diagnostic, pd *protocol.Diagnostic, kind protocol.CodeActionKind) (*protocol.CodeAction, error) {
340-
// The fix depends on the category of the analyzer. The diagnostic may be
341-
// nil, so use the error's category.
342-
analyzer := diagnosticToAnalyzer(snapshot, string(sd.Source), sd.Message)
343-
if analyzer == nil {
344-
return nil, fmt.Errorf("no convenience analyzer for source %s", sd.Source)
345-
}
346-
if analyzer.Fix == "" {
347-
return nil, fmt.Errorf("no fix for convenience analyzer %s", analyzer.Analyzer.Name)
348-
}
349-
cmd, err := command.NewApplyFixCommand(sd.Message, command.ApplyFixArgs{
350-
URI: protocol.URIFromSpanURI(sd.URI),
351-
Range: sd.Range,
352-
Fix: analyzer.Fix,
353-
})
354-
if err != nil {
355-
return nil, err
356-
}
357-
var diagnostics []protocol.Diagnostic
358-
if pd != nil {
359-
diagnostics = append(diagnostics, *pd)
360-
}
361-
return &protocol.CodeAction{
362-
Title: sd.Message,
363-
Kind: kind,
364-
Diagnostics: diagnostics,
365-
Command: &cmd,
366-
}, nil
367-
}
368-
369281
func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
370282
if rng.Start == rng.End {
371283
return nil, nil
@@ -431,47 +343,63 @@ func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) [
431343
}
432344
}
433345

434-
func quickFixesForDiagnostics(ctx context.Context, snapshot source.Snapshot, pdiags []protocol.Diagnostic, sdiags []*source.Diagnostic) ([]protocol.CodeAction, error) {
435-
var quickFixes []protocol.CodeAction
436-
for _, e := range sdiags {
346+
func codeActionsMatchingDiagnostics(ctx context.Context, snapshot source.Snapshot, pdiags []protocol.Diagnostic, sdiags []*source.Diagnostic) ([]protocol.CodeAction, error) {
347+
var actions []protocol.CodeAction
348+
for _, sd := range sdiags {
437349
var diag *protocol.Diagnostic
438-
for _, d := range pdiags {
439-
if sameDiagnostic(d, e) {
440-
diag = &d
350+
for _, pd := range pdiags {
351+
if sameDiagnostic(pd, sd) {
352+
diag = &pd
441353
break
442354
}
443355
}
444356
if diag == nil {
445357
continue
446358
}
447-
for _, fix := range e.SuggestedFixes {
448-
action := protocol.CodeAction{
449-
Title: fix.Title,
450-
Kind: protocol.QuickFix,
451-
Diagnostics: []protocol.Diagnostic{*diag},
452-
Edit: protocol.WorkspaceEdit{},
453-
Command: fix.Command,
454-
}
359+
diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, diag)
360+
if err != nil {
361+
return nil, err
362+
}
363+
actions = append(actions, diagActions...)
455364

456-
for uri, edits := range fix.Edits {
457-
fh, err := snapshot.GetVersionedFile(ctx, uri)
458-
if err != nil {
459-
return nil, err
460-
}
461-
action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, protocol.TextDocumentEdit{
462-
TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
463-
Version: fh.Version(),
464-
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
465-
URI: protocol.URIFromSpanURI(uri),
466-
},
467-
},
468-
Edits: edits,
469-
})
365+
}
366+
return actions, nil
367+
}
368+
369+
func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd *source.Diagnostic, pd *protocol.Diagnostic) ([]protocol.CodeAction, error) {
370+
var actions []protocol.CodeAction
371+
for _, fix := range sd.SuggestedFixes {
372+
action := protocol.CodeAction{
373+
Title: fix.Title,
374+
Kind: protocol.QuickFix,
375+
Edit: protocol.WorkspaceEdit{},
376+
Command: fix.Command,
377+
}
378+
if pd != nil {
379+
action.Diagnostics = []protocol.Diagnostic{*pd}
380+
}
381+
if sd.Analyzer != nil && sd.Analyzer.ActionKind != "" {
382+
action.Kind = sd.Analyzer.ActionKind
383+
}
384+
385+
for uri, edits := range fix.Edits {
386+
fh, err := snapshot.GetVersionedFile(ctx, uri)
387+
if err != nil {
388+
return nil, err
470389
}
471-
quickFixes = append(quickFixes, action)
390+
action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, protocol.TextDocumentEdit{
391+
TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
392+
Version: fh.Version(),
393+
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
394+
URI: protocol.URIFromSpanURI(uri),
395+
},
396+
},
397+
Edits: edits,
398+
})
472399
}
400+
actions = append(actions, action)
473401
}
474-
return quickFixes, nil
402+
return actions, nil
475403
}
476404

477405
func sameDiagnostic(pd protocol.Diagnostic, sd *source.Diagnostic) bool {

internal/lsp/diagnostics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg
265265
s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, pkgDiagnostics[cgf.URI])
266266
}
267267
if includeAnalysis && !pkg.HasListOrParseErrors() {
268-
reports, err := source.Analyze(ctx, snapshot, pkg)
268+
reports, err := source.Analyze(ctx, snapshot, pkg, false)
269269
if err != nil {
270270
event.Error(ctx, "warning: analyzing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID()))
271271
return

0 commit comments

Comments
 (0)