1 // Copyright 2020 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
17 "golang.org/x/tools/internal/lsp/diff"
18 "golang.org/x/tools/internal/lsp/diff/myers"
19 "golang.org/x/tools/internal/lsp/protocol"
20 "golang.org/x/tools/internal/lsp/source"
21 "golang.org/x/tools/internal/lsp/source/completion"
22 "golang.org/x/tools/internal/span"
25 // DiffLinks takes the links we got and checks if they are located within the source or a Note.
26 // If the link is within a Note, the link is removed.
27 // Returns an diff comment if there are differences and empty string if no diffs.
28 func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string {
29 var notePositions []token.Position
30 links := make(map[span.Span]string, len(wantLinks))
31 for _, link := range wantLinks {
32 links[link.Src] = link.Target
33 notePositions = append(notePositions, link.NotePosition)
35 for _, link := range gotLinks {
36 spn, err := mapper.RangeSpan(link.Range)
38 return fmt.Sprintf("%v", err)
41 for _, notePosition := range notePositions {
42 // Drop the links found inside expectation notes arguments as this links are not collected by expect package.
43 if notePosition.Line == spn.Start().Line() &&
44 notePosition.Column <= spn.Start().Column() {
52 if target, ok := links[spn]; ok {
54 if target != link.Target {
55 return fmt.Sprintf("for %v want %v, got %v\n", spn, target, link.Target)
58 return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target)
61 for spn, target := range links {
62 return fmt.Sprintf("missing link %v:%v\n", spn, target)
67 // DiffSymbols prints the diff between expected and actual symbols test results.
68 func DiffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string {
69 sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
70 sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name })
71 if len(got) != len(want) {
72 return summarizeSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want))
74 for i, w := range want {
77 return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
80 return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
82 if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 {
83 return summarizeSymbols(i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange)
85 if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" {
86 return fmt.Sprintf("children of %s: %s", w.Name, msg)
92 func summarizeSymbols(i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
93 msg := &bytes.Buffer{}
94 fmt.Fprint(msg, "document symbols failed")
96 fmt.Fprintf(msg, " at %d", i)
98 fmt.Fprint(msg, " because of ")
99 fmt.Fprintf(msg, reason, args...)
100 fmt.Fprint(msg, ":\nexpected:\n")
101 for _, s := range want {
102 fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
104 fmt.Fprintf(msg, "got:\n")
105 for _, s := range got {
106 fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
111 // FilterWorkspaceSymbols filters to got contained in the given dirs.
112 func FilterWorkspaceSymbols(got []protocol.SymbolInformation, dirs map[string]struct{}) []protocol.SymbolInformation {
113 var result []protocol.SymbolInformation
114 for _, si := range got {
115 if _, ok := dirs[filepath.Dir(si.Location.URI.SpanURI().Filename())]; ok {
116 result = append(result, si)
122 // DiffWorkspaceSymbols prints the diff between expected and actual workspace
123 // symbols test results.
124 func DiffWorkspaceSymbols(want, got []protocol.SymbolInformation) string {
125 sort.Slice(want, func(i, j int) bool { return fmt.Sprintf("%v", want[i]) < fmt.Sprintf("%v", want[j]) })
126 sort.Slice(got, func(i, j int) bool { return fmt.Sprintf("%v", got[i]) < fmt.Sprintf("%v", got[j]) })
127 if len(got) != len(want) {
128 return summarizeWorkspaceSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want))
130 for i, w := range want {
132 if w.Name != g.Name {
133 return summarizeWorkspaceSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
135 if w.Kind != g.Kind {
136 return summarizeWorkspaceSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
138 if w.Location.URI != g.Location.URI {
139 return summarizeWorkspaceSymbols(i, want, got, "incorrect uri got %v want %v", g.Location.URI, w.Location.URI)
141 if protocol.CompareRange(w.Location.Range, g.Location.Range) != 0 {
142 return summarizeWorkspaceSymbols(i, want, got, "incorrect range got %v want %v", g.Location.Range, w.Location.Range)
148 func summarizeWorkspaceSymbols(i int, want, got []protocol.SymbolInformation, reason string, args ...interface{}) string {
149 msg := &bytes.Buffer{}
150 fmt.Fprint(msg, "workspace symbols failed")
152 fmt.Fprintf(msg, " at %d", i)
154 fmt.Fprint(msg, " because of ")
155 fmt.Fprintf(msg, reason, args...)
156 fmt.Fprint(msg, ":\nexpected:\n")
157 for _, s := range want {
158 fmt.Fprintf(msg, " %v %v %v:%v\n", s.Name, s.Kind, s.Location.URI, s.Location.Range)
160 fmt.Fprintf(msg, "got:\n")
161 for _, s := range got {
162 fmt.Fprintf(msg, " %v %v %v:%v\n", s.Name, s.Kind, s.Location.URI, s.Location.Range)
167 // DiffDiagnostics prints the diff between expected and actual diagnostics test
169 func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string {
170 source.SortDiagnostics(want)
171 source.SortDiagnostics(got)
173 if len(got) != len(want) {
174 return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
176 for i, w := range want {
178 if w.Message != g.Message {
179 return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
181 if w.Severity != g.Severity {
182 return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
184 if w.Source != g.Source {
185 return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
187 // Don't check the range on the badimport test.
188 if strings.Contains(uri.Filename(), "badimport") {
191 if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
192 return summarizeDiagnostics(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
194 if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range.
195 if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
196 return summarizeDiagnostics(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
203 func summarizeDiagnostics(i int, uri span.URI, want, got []*source.Diagnostic, reason string, args ...interface{}) string {
204 msg := &bytes.Buffer{}
205 fmt.Fprint(msg, "diagnostics failed")
207 fmt.Fprintf(msg, " at %d", i)
209 fmt.Fprint(msg, " because of ")
210 fmt.Fprintf(msg, reason, args...)
211 fmt.Fprint(msg, ":\nexpected:\n")
212 for _, d := range want {
213 fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message)
215 fmt.Fprintf(msg, "got:\n")
216 for _, d := range got {
217 fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message)
222 func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string {
226 if len(got) != len(want) {
227 return summarizeCodeLens(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
229 for i, w := range want {
231 if w.Command.Command != g.Command.Command {
232 return summarizeCodeLens(i, uri, want, got, "incorrect Command Name got %v want %v", g.Command.Command, w.Command.Command)
234 if w.Command.Title != g.Command.Title {
235 return summarizeCodeLens(i, uri, want, got, "incorrect Command Title got %v want %v", g.Command.Title, w.Command.Title)
237 if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
238 return summarizeCodeLens(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
240 if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the codelens returns a zero-length range.
241 if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
242 return summarizeCodeLens(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
249 func sortCodeLens(c []protocol.CodeLens) {
250 sort.Slice(c, func(i int, j int) bool {
251 if r := protocol.CompareRange(c[i].Range, c[j].Range); r != 0 {
254 if c[i].Command.Command < c[j].Command.Command {
256 } else if c[i].Command.Command == c[j].Command.Command {
257 return c[i].Command.Title < c[j].Command.Title
264 func summarizeCodeLens(i int, uri span.URI, want, got []protocol.CodeLens, reason string, args ...interface{}) string {
265 msg := &bytes.Buffer{}
266 fmt.Fprint(msg, "codelens failed")
268 fmt.Fprintf(msg, " at %d", i)
270 fmt.Fprint(msg, " because of ")
271 fmt.Fprintf(msg, reason, args...)
272 fmt.Fprint(msg, ":\nexpected:\n")
273 for _, d := range want {
274 fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
276 fmt.Fprintf(msg, "got:\n")
277 for _, d := range got {
278 fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
283 func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) string {
284 decorate := func(f string, args ...interface{}) string {
285 return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
287 if len(got.Signatures) != 1 {
288 return decorate("wanted 1 signature, got %d", len(got.Signatures))
290 if got.ActiveSignature != 0 {
291 return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature))
293 if want.ActiveParameter != got.ActiveParameter {
294 return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter))
296 g := got.Signatures[0]
297 w := want.Signatures[0]
298 if w.Label != g.Label {
299 wLabel := w.Label + "\n"
300 d := myers.ComputeEdits("", wLabel, g.Label+"\n")
301 return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d))
303 var paramParts []string
304 for _, p := range g.Parameters {
305 paramParts = append(paramParts, p.Label)
307 paramsStr := strings.Join(paramParts, ", ")
308 if !strings.Contains(g.Label, paramsStr) {
309 return decorate("expected signature %q to contain params %q", g.Label, paramsStr)
314 // DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies
315 func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string {
316 expected := make(map[protocol.Location]bool)
317 for _, call := range expectedCalls {
318 expected[protocol.Location{URI: call.URI, Range: call.Range}] = true
321 got := make(map[protocol.Location]bool)
322 for _, call := range gotCalls {
323 got[protocol.Location{URI: call.URI, Range: call.Range}] = true
325 if len(got) != len(expected) {
326 return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got))
328 for spn := range got {
330 return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got)
336 func ToProtocolCompletionItems(items []completion.CompletionItem) []protocol.CompletionItem {
337 var result []protocol.CompletionItem
338 for _, item := range items {
339 result = append(result, ToProtocolCompletionItem(item))
344 func ToProtocolCompletionItem(item completion.CompletionItem) protocol.CompletionItem {
345 pItem := protocol.CompletionItem{
349 Documentation: item.Documentation,
350 InsertText: item.InsertText,
351 TextEdit: &protocol.TextEdit{
352 NewText: item.Snippet(),
354 // Negate score so best score has lowest sort text like real API.
355 SortText: fmt.Sprint(-item.Score),
357 if pItem.InsertText == "" {
358 pItem.InsertText = pItem.Label
363 func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem {
365 got []protocol.CompletionItem
366 wantBuiltins = strings.Contains(string(src.URI()), "builtins")
367 wantKeywords = strings.Contains(string(src.URI()), "keywords")
369 for _, item := range items {
370 if !wantBuiltins && isBuiltin(item.Label, item.Detail, item.Kind) {
374 if !wantKeywords && token.Lookup(item.Label).IsKeyword() {
378 got = append(got, item)
383 func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool {
384 if detail == "" && kind == protocol.ClassCompletion {
387 // Remaining builtin constants, variables, interfaces, and functions.
389 if i := strings.Index(trimmed, "("); i >= 0 {
390 trimmed = trimmed[:i]
393 case "append", "cap", "close", "complex", "copy", "delete",
394 "error", "false", "imag", "iota", "len", "make", "new",
395 "nil", "panic", "print", "println", "real", "recover", "true":
401 func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string {
407 errorMsg = "completions out of order"
409 for _, w := range want {
411 for i, g := range got {
412 if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind {
413 matchedIdxs = append(matchedIdxs, i)
421 sort, _ := strconv.ParseFloat(g.SortText, 64)
422 if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort {
424 errorMsg = "candidate scores not strictly decreasing"
432 return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion")
436 sort.Ints(matchedIdxs)
437 matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
438 for _, idx := range matchedIdxs {
439 matched = append(matched, got[idx])
443 return summarizeCompletionItems(-1, want, matched, errorMsg)
449 func DiffSnippets(want string, got *protocol.CompletionItem) string {
453 return fmt.Sprintf("expected no snippet but got %s", x.NewText)
457 return fmt.Sprintf("couldn't find completion matching %q", want)
460 if want != x.NewText {
461 return fmt.Sprintf("expected snippet %q, got %q", want, x.NewText)
467 func FindItem(list []protocol.CompletionItem, want completion.CompletionItem) *protocol.CompletionItem {
468 for _, item := range list {
469 if item.Label == want.Label {
476 // DiffCompletionItems prints the diff between expected and actual completion
478 func DiffCompletionItems(want, got []protocol.CompletionItem) string {
479 if len(got) != len(want) {
480 return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
482 for i, w := range want {
484 if w.Label != g.Label {
485 return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
487 if w.Detail != g.Detail {
488 return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
490 if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
491 if w.Documentation != g.Documentation {
492 return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
495 if w.Kind != g.Kind {
496 return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
502 func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string {
503 msg := &bytes.Buffer{}
504 fmt.Fprint(msg, "completion failed")
506 fmt.Fprintf(msg, " at %d", i)
508 fmt.Fprint(msg, " because of ")
509 fmt.Fprintf(msg, reason, args...)
510 fmt.Fprint(msg, ":\nexpected:\n")
511 for _, d := range want {
512 fmt.Fprintf(msg, " %v\n", d)
514 fmt.Fprintf(msg, "got:\n")
515 for _, d := range got {
516 fmt.Fprintf(msg, " %v\n", d)
521 func EnableAllAnalyzers(view source.View, opts *source.Options) {
522 if opts.Analyses == nil {
523 opts.Analyses = make(map[string]bool)
525 for _, a := range opts.DefaultAnalyzers {
526 if !a.IsEnabled(view) {
527 opts.Analyses[a.Analyzer.Name] = true
530 for _, a := range opts.TypeErrorAnalyzers {
531 if !a.IsEnabled(view) {
532 opts.Analyses[a.Analyzer.Name] = true
535 for _, a := range opts.ConvenienceAnalyzers {
536 if !a.IsEnabled(view) {
537 opts.Analyses[a.Analyzer.Name] = true
540 for _, a := range opts.StaticcheckAnalyzers {
541 if !a.IsEnabled(view) {
542 opts.Analyses[a.Analyzer.Name] = true
547 func Diff(want, got string) string {
551 // Add newlines to avoid newline messages in diff.
554 d := myers.ComputeEdits("", want, got)
555 return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d))