Skip to content

Commit e106694

Browse files
committed
gopls/internal/lsp: bundle certain quick-fixes with their diagnostic
To pragmatically avoid re-diagnosing the entire workspace, we can bundle quick-fixes directly with their corresponding diagnostic, using the Diagnostic.Data field added for this purpose in version 3.16 of the LSP spec. We should use this mechanism more generally, but for fixes with edits we'd have to be careful that the edits are still valid in the current snapshot. For now, be surgical. This is the final regression we're tracking in the incremental gopls issue (golang/go#57987). Fixes golang/go#57987 Change-Id: Iaca91484e90341d677ecf573944edffef6e07255 Reviewed-on: https://go-review.googlesource.com/c/tools/+/497398 TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 5dc3f74 commit e106694

File tree

8 files changed

+128
-19
lines changed

8 files changed

+128
-19
lines changed

gopls/internal/lsp/cache/check.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -1542,14 +1542,18 @@ func depsErrors(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs
15421542
if err != nil {
15431543
return nil, err
15441544
}
1545-
errors = append(errors, &source.Diagnostic{
1545+
diag := &source.Diagnostic{
15461546
URI: imp.cgf.URI,
15471547
Range: rng,
15481548
Severity: protocol.SeverityError,
15491549
Source: source.TypeError,
15501550
Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err),
15511551
SuggestedFixes: fixes,
1552-
})
1552+
}
1553+
if !source.BundleQuickFixes(diag) {
1554+
bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message)
1555+
}
1556+
errors = append(errors, diag)
15531557
}
15541558
}
15551559
}
@@ -1585,14 +1589,18 @@ func depsErrors(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs
15851589
if err != nil {
15861590
return nil, err
15871591
}
1588-
errors = append(errors, &source.Diagnostic{
1592+
diag := &source.Diagnostic{
15891593
URI: pm.URI,
15901594
Range: rng,
15911595
Severity: protocol.SeverityError,
15921596
Source: source.TypeError,
15931597
Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err),
15941598
SuggestedFixes: fixes,
1595-
})
1599+
}
1600+
if !source.BundleQuickFixes(diag) {
1601+
bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message)
1602+
}
1603+
errors = append(errors, diag)
15961604
break
15971605
}
15981606
}

gopls/internal/lsp/code_action.go

+11
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,17 @@ func documentChanges(fh source.FileHandle, edits []protocol.TextEdit) []protocol
473473

474474
func codeActionsMatchingDiagnostics(ctx context.Context, snapshot source.Snapshot, pdiags []protocol.Diagnostic, sdiags []*source.Diagnostic) ([]protocol.CodeAction, error) {
475475
var actions []protocol.CodeAction
476+
var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field
477+
for _, pd := range pdiags {
478+
bundled := source.BundledQuickFixes(pd)
479+
if len(bundled) > 0 {
480+
actions = append(actions, bundled...)
481+
} else {
482+
// No bundled actions: keep searching for a match.
483+
unbundled = append(unbundled, pd)
484+
}
485+
}
486+
476487
for _, sd := range sdiags {
477488
var diag *protocol.Diagnostic
478489
for _, pd := range pdiags {

gopls/internal/lsp/diagnostics.go

+4
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ func computeDiagnosticHash(diags ...*source.Diagnostic) string {
145145
fmt.Fprintf(h, "range: %s\n", d.Range)
146146
fmt.Fprintf(h, "severity: %s\n", d.Severity)
147147
fmt.Fprintf(h, "source: %s\n", d.Source)
148+
if d.BundledFixes != nil {
149+
fmt.Fprintf(h, "fixes: %s\n", *d.BundledFixes)
150+
}
148151
}
149152
return fmt.Sprintf("%x", h.Sum(nil))
150153
}
@@ -771,6 +774,7 @@ func toProtocolDiagnostics(diagnostics []*source.Diagnostic) []protocol.Diagnost
771774
Source: string(diag.Source),
772775
Tags: emptySliceDiagnosticTag(diag.Tags),
773776
RelatedInformation: diag.Related,
777+
Data: diag.BundledFixes,
774778
}
775779
if diag.Code != "" {
776780
pdiag.Code = diag.Code

gopls/internal/lsp/protocol/generate/tables.go

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ var renameProp = map[prop]string{
6868
{"Command", "arguments"}: "[]json.RawMessage",
6969
{"CompletionItem", "textEdit"}: "TextEdit",
7070
{"Diagnostic", "code"}: "interface{}",
71+
{"Diagnostic", "data"}: "json.RawMessage", // delay unmarshalling quickfixes
7172

7273
{"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]interface{}",
7374

gopls/internal/lsp/protocol/tsprotocol.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/lsp/source/diagnostics.go

+80
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package source
66

77
import (
88
"context"
9+
"encoding/json"
910

11+
"golang.org/x/tools/gopls/internal/bug"
1012
"golang.org/x/tools/gopls/internal/lsp/protocol"
1113
"golang.org/x/tools/gopls/internal/span"
1214
)
@@ -136,3 +138,81 @@ func CombineDiagnostics(tdiags []*Diagnostic, adiags []*Diagnostic, outT, outA *
136138

137139
*outT = append(*outT, tdiags...)
138140
}
141+
142+
// quickFixesJSON is a JSON-serializable list of quick fixes
143+
// to be saved in the protocol.Diagnostic.Data field.
144+
type quickFixesJSON struct {
145+
// TODO(rfindley): pack some sort of identifier here for later
146+
// lookup/validation?
147+
Fixes []protocol.CodeAction
148+
}
149+
150+
// BundleQuickFixes attempts to bundle sd.SuggestedFixes into the
151+
// sd.BundledFixes field, so that it can be round-tripped through the client.
152+
// It returns false if the quick-fixes cannot be bundled.
153+
func BundleQuickFixes(sd *Diagnostic) bool {
154+
if len(sd.SuggestedFixes) == 0 {
155+
return true
156+
}
157+
var actions []protocol.CodeAction
158+
for _, fix := range sd.SuggestedFixes {
159+
if fix.Edits != nil {
160+
// For now, we only support bundled code actions that execute commands.
161+
//
162+
// In order to cleanly support bundled edits, we'd have to guarantee that
163+
// the edits were generated on the current snapshot. But this naively
164+
// implies that every fix would have to include a snapshot ID, which
165+
// would require us to republish all diagnostics on each new snapshot.
166+
//
167+
// TODO(rfindley): in order to avoid this additional chatter, we'd need
168+
// to build some sort of registry or other mechanism on the snapshot to
169+
// check whether a diagnostic is still valid.
170+
return false
171+
}
172+
action := protocol.CodeAction{
173+
Title: fix.Title,
174+
Kind: fix.ActionKind,
175+
Command: fix.Command,
176+
}
177+
actions = append(actions, action)
178+
}
179+
fixes := quickFixesJSON{
180+
Fixes: actions,
181+
}
182+
data, err := json.Marshal(fixes)
183+
if err != nil {
184+
bug.Reportf("marshalling quick fixes: %v", err)
185+
return false
186+
}
187+
msg := json.RawMessage(data)
188+
sd.BundledFixes = &msg
189+
return true
190+
}
191+
192+
// BundledQuickFixes extracts any bundled codeActions from the
193+
// diag.Data field.
194+
func BundledQuickFixes(diag protocol.Diagnostic) []protocol.CodeAction {
195+
if diag.Data == nil {
196+
return nil
197+
}
198+
var fix quickFixesJSON
199+
if err := json.Unmarshal(*diag.Data, &fix); err != nil {
200+
bug.Reportf("unmarshalling quick fix: %v", err)
201+
return nil
202+
}
203+
204+
var actions []protocol.CodeAction
205+
for _, action := range fix.Fixes {
206+
// See BundleQuickFixes: for now we only support bundling commands.
207+
if action.Edit != nil {
208+
bug.Reportf("bundled fix %q includes workspace edits", action.Title)
209+
continue
210+
}
211+
// associate the action with the incoming diagnostic
212+
// (Note that this does not mutate the fix.Fixes slice).
213+
action.Diagnostics = []protocol.Diagnostic{diag}
214+
actions = append(actions, action)
215+
}
216+
217+
return actions
218+
}

gopls/internal/lsp/source/view.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"context"
1010
"crypto/sha256"
11+
"encoding/json"
1112
"errors"
1213
"fmt"
1314
"go/ast"
@@ -971,7 +972,18 @@ type Diagnostic struct {
971972
Related []protocol.DiagnosticRelatedInformation
972973

973974
// Fields below are used internally to generate quick fixes. They aren't
974-
// part of the LSP spec and don't leave the server.
975+
// part of the LSP spec and historically didn't leave the server.
976+
//
977+
// Update(2023-05): version 3.16 of the LSP spec included support for the
978+
// Diagnostic.data field, which holds arbitrary data preserved in the
979+
// diagnostic for codeAction requests. This field allows bundling additional
980+
// information for quick-fixes, and gopls can (and should) use this
981+
// information to avoid re-evaluating diagnostics in code-action handlers.
982+
//
983+
// In order to stage this transition incrementally, the 'BundledFixes' field
984+
// may store a 'bundled' (=json-serialized) form of the associated
985+
// SuggestedFixes. Not all diagnostics have their fixes bundled.
986+
BundledFixes *json.RawMessage
975987
SuggestedFixes []SuggestedFix
976988
}
977989

gopls/internal/regtest/modfile/modfile_test.go

+6-13
Original file line numberDiff line numberDiff line change
@@ -498,14 +498,8 @@ var _ = blah.Name
498498
ReadDiagnostics("a/go.mod", &modDiags),
499499
)
500500

501-
// golang.go#57987: now that gopls is incremental, we must be careful where
502-
// we request diagnostics. We must design a simpler way to correlate
503-
// published diagnostics with subsequent code action requests (see also the
504-
// comment in Server.codeAction).
505-
const canRequestCodeActionsForWorkspaceDiagnostics = false
506-
if canRequestCodeActionsForWorkspaceDiagnostics {
507-
env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics)
508-
const want = `module mod.com
501+
env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics)
502+
const want = `module mod.com
509503
510504
go 1.12
511505
@@ -514,11 +508,10 @@ require (
514508
example.com/blah/v2 v2.0.0
515509
)
516510
`
517-
env.SaveBuffer("a/go.mod")
518-
env.AfterChange(NoDiagnostics(ForFile("a/main.go")))
519-
if got := env.BufferText("a/go.mod"); got != want {
520-
t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got))
521-
}
511+
env.SaveBuffer("a/go.mod")
512+
env.AfterChange(NoDiagnostics(ForFile("a/main.go")))
513+
if got := env.BufferText("a/go.mod"); got != want {
514+
t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got))
522515
}
523516
})
524517
}

0 commit comments

Comments
 (0)