Skip to content

Commit 6492058

Browse files
committed
gopls: strict LSP compliance for empty slices
There are a number of places in the LSP where []T is required and nil is unacceptable. This change attempts to ensure that gopls generates the correct data. The change would like to use a generic function, but instead uses 4 functions, one for each of string, TextEdit, DiagnosticTag, and CompletionItemTag. There are also many places that call for []T, but nil is acceptable, as the json tag include 'omitempty'. These don't need any changes. This change corrects the existing code, but changes in either gopls or the LSP could introduce new instances of the bug. On the other hand, this bug has been present for a long time, and was only now found, by a strict LSP client that didn't use semantic tokens. Fixes: golang/go#59479 Change-Id: I00f30e6b50eaf7768bc6c39859c64dc350181f20 Reviewed-on: https://go-review.googlesource.com/c/tools/+/483216 gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Peter Weinberger <[email protected]>
1 parent 0f415ef commit 6492058

File tree

13 files changed

+76
-31
lines changed

13 files changed

+76
-31
lines changed

gopls/internal/lsp/cmd/suggested_fix.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error {
8080
if err != nil {
8181
return err
8282
}
83+
if file.diagnostics == nil {
84+
// LSP requires a slice, not a nil.
85+
file.diagnostics = []protocol.Diagnostic{}
86+
}
8387
p := protocol.CodeActionParams{
8488
TextDocument: protocol.TextDocumentIdentifier{
8589
URI: protocol.URIFromSpanURI(uri),

gopls/internal/lsp/code_action.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ func documentChanges(fh source.FileHandle, edits []protocol.TextEdit) []protocol
405405
URI: protocol.URIFromSpanURI(fh.URI()),
406406
},
407407
},
408-
Edits: edits,
408+
Edits: nonNilSliceTextEdit(edits),
409409
},
410410
},
411411
}
@@ -437,7 +437,7 @@ func codeActionsMatchingDiagnostics(ctx context.Context, snapshot source.Snapsho
437437
func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd *source.Diagnostic, pd *protocol.Diagnostic) ([]protocol.CodeAction, error) {
438438
var actions []protocol.CodeAction
439439
for _, fix := range sd.SuggestedFixes {
440-
var changes []protocol.DocumentChanges
440+
changes := []protocol.DocumentChanges{} // must be a slice
441441
for uri, edits := range fix.Edits {
442442
fh, err := snapshot.ReadFile(ctx, uri)
443443
if err != nil {

gopls/internal/lsp/command.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs
159159
if err != nil {
160160
return err
161161
}
162-
var changes []protocol.DocumentChanges
162+
changes := []protocol.DocumentChanges{} // must be a slice
163163
for _, edit := range edits {
164164
edit := edit
165165
changes = append(changes, protocol.DocumentChanges{
@@ -376,7 +376,7 @@ func (c *commandHandler) RemoveDependency(ctx context.Context, args command.Remo
376376
URI: protocol.URIFromSpanURI(deps.fh.URI()),
377377
},
378378
},
379-
Edits: edits,
379+
Edits: nonNilSliceTextEdit(edits),
380380
},
381381
},
382382
},
@@ -591,7 +591,7 @@ func (s *Server) runGoModUpdateCommands(ctx context.Context, snapshot source.Sna
591591
if len(changes) == 0 {
592592
return nil
593593
}
594-
var documentChanges []protocol.DocumentChanges
594+
documentChanges := []protocol.DocumentChanges{} // must be a slice
595595
for _, change := range changes {
596596
change := change
597597
documentChanges = append(documentChanges, protocol.DocumentChanges{

gopls/internal/lsp/completion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func toProtocolCompletionItems(candidates []completion.CompletionItem, rng proto
134134

135135
Preselect: i == 0,
136136
Documentation: doc,
137-
Tags: candidate.Tags,
137+
Tags: nonNilSliceCompletionItemTag(candidate.Tags),
138138
Deprecated: candidate.Deprecated,
139139
}
140140
items = append(items, item)

gopls/internal/lsp/completion_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func expected(t *testing.T, test tests.Completion, items tests.CompletionItems)
118118
Label: item.Label,
119119
Kind: item.Kind,
120120
Detail: item.Detail,
121+
Tags: []protocol.CompletionItemTag{}, // must be a slice
121122
Documentation: &protocol.Or_CompletionItem_documentation{
122123
Value: item.Documentation,
123124
},

gopls/internal/lsp/diagnostics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ func toProtocolDiagnostics(diagnostics []*source.Diagnostic) []protocol.Diagnost
793793
Range: diag.Range,
794794
Severity: diag.Severity,
795795
Source: string(diag.Source),
796-
Tags: diag.Tags,
796+
Tags: emptySliceDiagnosticTag(diag.Tags),
797797
RelatedInformation: diag.Related,
798798
}
799799
if diag.Code != "" {

gopls/internal/lsp/general.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ See https://github.com/golang/go/issues/45732 for more information.`,
152152
DocumentSymbolProvider: &protocol.Or_ServerCapabilities_documentSymbolProvider{Value: true},
153153
WorkspaceSymbolProvider: &protocol.Or_ServerCapabilities_workspaceSymbolProvider{Value: true},
154154
ExecuteCommandProvider: &protocol.ExecuteCommandOptions{
155-
Commands: options.SupportedCommands,
155+
Commands: nonNilSliceString(options.SupportedCommands),
156156
},
157157
FoldingRangeProvider: &protocol.Or_ServerCapabilities_foldingRangeProvider{Value: true},
158158
HoverProvider: &protocol.Or_ServerCapabilities_hoverProvider{Value: true},
@@ -166,8 +166,8 @@ See https://github.com/golang/go/issues/45732 for more information.`,
166166
Range: &protocol.Or_SemanticTokensOptions_range{Value: true},
167167
Full: &protocol.Or_SemanticTokensOptions_full{Value: true},
168168
Legend: protocol.SemanticTokensLegend{
169-
TokenTypes: s.session.Options().SemanticTypes,
170-
TokenModifiers: s.session.Options().SemanticMods,
169+
TokenTypes: nonNilSliceString(s.session.Options().SemanticTypes),
170+
TokenModifiers: nonNilSliceString(s.session.Options().SemanticMods),
171171
},
172172
},
173173
SignatureHelpProvider: &protocol.SignatureHelpOptions{
@@ -452,7 +452,7 @@ func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns
452452
for k := range s.watchedGlobPatterns {
453453
delete(s.watchedGlobPatterns, k)
454454
}
455-
var watchers []protocol.FileSystemWatcher
455+
watchers := []protocol.FileSystemWatcher{} // must be a slice
456456
val := protocol.WatchChange | protocol.WatchDelete | protocol.WatchCreate
457457
for pattern := range patterns {
458458
watchers = append(watchers, protocol.FileSystemWatcher{
@@ -629,3 +629,31 @@ func (s *Server) exit(ctx context.Context) error {
629629
// close naturally if needed after the connection is closed.
630630
return nil
631631
}
632+
633+
// TODO: when we can assume go1.18, replace with generic
634+
// (after retiring support for go1.17)
635+
func nonNilSliceString(x []string) []string {
636+
if x == nil {
637+
return []string{}
638+
}
639+
return x
640+
}
641+
func nonNilSliceTextEdit(x []protocol.TextEdit) []protocol.TextEdit {
642+
if x == nil {
643+
return []protocol.TextEdit{}
644+
}
645+
646+
return x
647+
}
648+
func nonNilSliceCompletionItemTag(x []protocol.CompletionItemTag) []protocol.CompletionItemTag {
649+
if x == nil {
650+
return []protocol.CompletionItemTag{}
651+
}
652+
return x
653+
}
654+
func emptySliceDiagnosticTag(x []protocol.DiagnosticTag) []protocol.DiagnosticTag {
655+
if x == nil {
656+
return []protocol.DiagnosticTag{}
657+
}
658+
return x
659+
}

gopls/internal/lsp/lsprpc/lsprpc_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package lsprpc
66

77
import (
88
"context"
9+
"encoding/json"
910
"errors"
1011
"regexp"
1112
"strings"
@@ -343,3 +344,30 @@ func TestListenParsing(t *testing.T) {
343344
}
344345
}
345346
}
347+
348+
// For #59479, verify that empty slices are serialized as [].
349+
func TestEmptySlices(t *testing.T) {
350+
// The LSP would prefer that empty slices be sent as [] rather than null.
351+
const bad = `{"a":null}`
352+
const good = `{"a":[]}`
353+
var x struct {
354+
A []string `json:"a"`
355+
}
356+
buf, _ := json.Marshal(x)
357+
if string(buf) != bad {
358+
// uninitialized is ezpected to give null
359+
t.Errorf("unexpectedly got %s, want %s", buf, bad)
360+
}
361+
x.A = make([]string, 0)
362+
buf, _ = json.Marshal(x)
363+
if string(buf) != good {
364+
// expect []
365+
t.Errorf("unexpectedly got %s, want %s", buf, good)
366+
}
367+
x.A = []string{}
368+
buf, _ = json.Marshal(x)
369+
if string(buf) != good {
370+
// expect []
371+
t.Errorf("unexpectedly got %s, want %s", buf, good)
372+
}
373+
}

gopls/internal/lsp/rename.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr
3131
return nil, err
3232
}
3333

34-
var docChanges []protocol.DocumentChanges
34+
docChanges := []protocol.DocumentChanges{} // must be a slice
3535
for uri, e := range edits {
3636
fh, err := snapshot.ReadFile(ctx, uri)
3737
if err != nil {

gopls/internal/lsp/source/format.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit
336336
return result, nil
337337
}
338338

339-
// ToProtocolEdits converts diff.Edits to LSP TextEdits.
339+
// ToProtocolEdits converts diff.Edits to a non-nil slice of LSP TextEdits.
340340
// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray
341341
func ToProtocolEdits(m *protocol.Mapper, edits []diff.Edit) ([]protocol.TextEdit, error) {
342342
// LSP doesn't require TextEditArray to be sorted:

0 commit comments

Comments
 (0)