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.
18 "golang.org/x/tools/internal/lsp/diff"
19 "golang.org/x/tools/internal/lsp/diff/myers"
20 "golang.org/x/tools/internal/lsp/protocol"
21 "golang.org/x/tools/internal/lsp/source"
22 "golang.org/x/tools/internal/lsp/source/completion"
23 "golang.org/x/tools/internal/span"
26 // DiffLinks takes the links we got and checks if they are located within the source or a Note.
27 // If the link is within a Note, the link is removed.
28 // Returns an diff comment if there are differences and empty string if no diffs.
29 func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string {
30 var notePositions []token.Position
31 links := make(map[span.Span]string, len(wantLinks))
32 for _, link := range wantLinks {
33 links[link.Src] = link.Target
34 notePositions = append(notePositions, link.NotePosition)
36 for _, link := range gotLinks {
37 spn, err := mapper.RangeSpan(link.Range)
39 return fmt.Sprintf("%v", err)
42 for _, notePosition := range notePositions {
43 // Drop the links found inside expectation notes arguments as this links are not collected by expect package.
44 if notePosition.Line == spn.Start().Line() &&
45 notePosition.Column <= spn.Start().Column() {
53 if target, ok := links[spn]; ok {
55 if target != link.Target {
56 return fmt.Sprintf("for %v want %v, got %v\n", spn, target, link.Target)
59 return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target)
62 for spn, target := range links {
63 return fmt.Sprintf("missing link %v:%v\n", spn, target)
68 // DiffSymbols prints the diff between expected and actual symbols test results.
69 func DiffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string {
70 sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
71 sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name })
72 if len(got) != len(want) {
73 return summarizeSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want))
75 for i, w := range want {
78 return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
81 return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
83 if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 {
84 return summarizeSymbols(i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange)
86 if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" {
87 return fmt.Sprintf("children of %s: %s", w.Name, msg)
93 func summarizeSymbols(i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
94 msg := &bytes.Buffer{}
95 fmt.Fprint(msg, "document symbols failed")
97 fmt.Fprintf(msg, " at %d", i)
99 fmt.Fprint(msg, " because of ")
100 fmt.Fprintf(msg, reason, args...)
101 fmt.Fprint(msg, ":\nexpected:\n")
102 for _, s := range want {
103 fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
105 fmt.Fprintf(msg, "got:\n")
106 for _, s := range got {
107 fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
112 // DiffDiagnostics prints the diff between expected and actual diagnostics test
114 func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string {
115 source.SortDiagnostics(want)
116 source.SortDiagnostics(got)
118 if len(got) != len(want) {
119 return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
121 for i, w := range want {
123 if w.Message != g.Message {
124 return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
126 if w.Severity != g.Severity {
127 return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
129 if w.Source != g.Source {
130 return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
132 if !rangeOverlaps(g.Range, w.Range) {
133 return summarizeDiagnostics(i, uri, want, got, "range %v does not overlap %v", g.Range, w.Range)
139 // rangeOverlaps reports whether r1 and r2 overlap.
140 func rangeOverlaps(r1, r2 protocol.Range) bool {
141 if inRange(r2.Start, r1) || inRange(r1.Start, r2) {
147 // inRange reports whether p is contained within [r.Start, r.End), or if p ==
148 // r.Start == r.End (special handling for the case where the range is a single
150 func inRange(p protocol.Position, r protocol.Range) bool {
151 if protocol.IsPoint(r) {
152 return protocol.ComparePosition(r.Start, p) == 0
154 if protocol.ComparePosition(r.Start, p) <= 0 && protocol.ComparePosition(p, r.End) < 0 {
160 func summarizeDiagnostics(i int, uri span.URI, want, got []*source.Diagnostic, reason string, args ...interface{}) string {
161 msg := &bytes.Buffer{}
162 fmt.Fprint(msg, "diagnostics failed")
164 fmt.Fprintf(msg, " at %d", i)
166 fmt.Fprint(msg, " because of ")
167 fmt.Fprintf(msg, reason, args...)
168 fmt.Fprint(msg, ":\nexpected:\n")
169 for _, d := range want {
170 fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message)
172 fmt.Fprintf(msg, "got:\n")
173 for _, d := range got {
174 fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message)
179 func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string {
183 if len(got) != len(want) {
184 return summarizeCodeLens(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
186 for i, w := range want {
188 if w.Command.Command != g.Command.Command {
189 return summarizeCodeLens(i, uri, want, got, "incorrect Command Name got %v want %v", g.Command.Command, w.Command.Command)
191 if w.Command.Title != g.Command.Title {
192 return summarizeCodeLens(i, uri, want, got, "incorrect Command Title got %v want %v", g.Command.Title, w.Command.Title)
194 if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
195 return summarizeCodeLens(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
197 if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the codelens returns a zero-length range.
198 if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
199 return summarizeCodeLens(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
206 func sortCodeLens(c []protocol.CodeLens) {
207 sort.Slice(c, func(i int, j int) bool {
208 if r := protocol.CompareRange(c[i].Range, c[j].Range); r != 0 {
211 if c[i].Command.Command < c[j].Command.Command {
213 } else if c[i].Command.Command == c[j].Command.Command {
214 return c[i].Command.Title < c[j].Command.Title
221 func summarizeCodeLens(i int, uri span.URI, want, got []protocol.CodeLens, reason string, args ...interface{}) string {
222 msg := &bytes.Buffer{}
223 fmt.Fprint(msg, "codelens failed")
225 fmt.Fprintf(msg, " at %d", i)
227 fmt.Fprint(msg, " because of ")
228 fmt.Fprintf(msg, reason, args...)
229 fmt.Fprint(msg, ":\nexpected:\n")
230 for _, d := range want {
231 fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
233 fmt.Fprintf(msg, "got:\n")
234 for _, d := range got {
235 fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
240 func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, error) {
241 decorate := func(f string, args ...interface{}) string {
242 return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
244 if len(got.Signatures) != 1 {
245 return decorate("wanted 1 signature, got %d", len(got.Signatures)), nil
247 if got.ActiveSignature != 0 {
248 return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)), nil
250 if want.ActiveParameter != got.ActiveParameter {
251 return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)), nil
253 g := got.Signatures[0]
254 w := want.Signatures[0]
255 if w.Label != g.Label {
256 wLabel := w.Label + "\n"
257 d, err := myers.ComputeEdits("", wLabel, g.Label+"\n")
261 return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d)), err
263 var paramParts []string
264 for _, p := range g.Parameters {
265 paramParts = append(paramParts, p.Label)
267 paramsStr := strings.Join(paramParts, ", ")
268 if !strings.Contains(g.Label, paramsStr) {
269 return decorate("expected signature %q to contain params %q", g.Label, paramsStr), nil
274 // DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies
275 func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string {
276 expected := make(map[protocol.Location]bool)
277 for _, call := range expectedCalls {
278 expected[protocol.Location{URI: call.URI, Range: call.Range}] = true
281 got := make(map[protocol.Location]bool)
282 for _, call := range gotCalls {
283 got[protocol.Location{URI: call.URI, Range: call.Range}] = true
285 if len(got) != len(expected) {
286 return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got))
288 for spn := range got {
290 return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got)
296 func ToProtocolCompletionItems(items []completion.CompletionItem) []protocol.CompletionItem {
297 var result []protocol.CompletionItem
298 for _, item := range items {
299 result = append(result, ToProtocolCompletionItem(item))
304 func ToProtocolCompletionItem(item completion.CompletionItem) protocol.CompletionItem {
305 pItem := protocol.CompletionItem{
309 Documentation: item.Documentation,
310 InsertText: item.InsertText,
311 TextEdit: &protocol.TextEdit{
312 NewText: item.Snippet(),
314 // Negate score so best score has lowest sort text like real API.
315 SortText: fmt.Sprint(-item.Score),
317 if pItem.InsertText == "" {
318 pItem.InsertText = pItem.Label
323 func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem {
325 got []protocol.CompletionItem
326 wantBuiltins = strings.Contains(string(src.URI()), "builtins")
327 wantKeywords = strings.Contains(string(src.URI()), "keywords")
329 for _, item := range items {
330 if !wantBuiltins && isBuiltin(item.Label, item.Detail, item.Kind) {
334 if !wantKeywords && token.Lookup(item.Label).IsKeyword() {
338 got = append(got, item)
343 func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool {
344 if detail == "" && kind == protocol.ClassCompletion {
347 // Remaining builtin constants, variables, interfaces, and functions.
349 if i := strings.Index(trimmed, "("); i >= 0 {
350 trimmed = trimmed[:i]
353 case "append", "cap", "close", "complex", "copy", "delete",
354 "error", "false", "imag", "iota", "len", "make", "new",
355 "nil", "panic", "print", "println", "real", "recover", "true":
361 func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string {
367 errorMsg = "completions out of order"
369 for _, w := range want {
371 for i, g := range got {
372 if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind {
373 matchedIdxs = append(matchedIdxs, i)
381 sort, _ := strconv.ParseFloat(g.SortText, 64)
382 if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort {
384 errorMsg = "candidate scores not strictly decreasing"
392 return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion")
396 sort.Ints(matchedIdxs)
397 matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
398 for _, idx := range matchedIdxs {
399 matched = append(matched, got[idx])
403 return summarizeCompletionItems(-1, want, matched, errorMsg)
409 func DiffSnippets(want string, got *protocol.CompletionItem) string {
413 return fmt.Sprintf("expected no snippet but got %s", x.NewText)
417 return fmt.Sprintf("couldn't find completion matching %q", want)
420 if want != x.NewText {
421 return fmt.Sprintf("expected snippet %q, got %q", want, x.NewText)
427 func FindItem(list []protocol.CompletionItem, want completion.CompletionItem) *protocol.CompletionItem {
428 for _, item := range list {
429 if item.Label == want.Label {
436 // DiffCompletionItems prints the diff between expected and actual completion
438 func DiffCompletionItems(want, got []protocol.CompletionItem) string {
439 if len(got) != len(want) {
440 return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
442 for i, w := range want {
444 if w.Label != g.Label {
445 return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
447 if w.Detail != g.Detail {
448 return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
450 if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
451 if w.Documentation != g.Documentation {
452 return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
455 if w.Kind != g.Kind {
456 return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
462 func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string {
463 msg := &bytes.Buffer{}
464 fmt.Fprint(msg, "completion failed")
466 fmt.Fprintf(msg, " at %d", i)
468 fmt.Fprint(msg, " because of ")
469 fmt.Fprintf(msg, reason, args...)
470 fmt.Fprint(msg, ":\nexpected:\n")
471 for _, d := range want {
472 fmt.Fprintf(msg, " %v\n", d)
474 fmt.Fprintf(msg, "got:\n")
475 for _, d := range got {
476 fmt.Fprintf(msg, " %v\n", d)
481 func EnableAllAnalyzers(view source.View, opts *source.Options) {
482 if opts.Analyses == nil {
483 opts.Analyses = make(map[string]bool)
485 for _, a := range opts.DefaultAnalyzers {
486 if !a.IsEnabled(view) {
487 opts.Analyses[a.Analyzer.Name] = true
490 for _, a := range opts.TypeErrorAnalyzers {
491 if !a.IsEnabled(view) {
492 opts.Analyses[a.Analyzer.Name] = true
495 for _, a := range opts.ConvenienceAnalyzers {
496 if !a.IsEnabled(view) {
497 opts.Analyses[a.Analyzer.Name] = true
500 for _, a := range opts.StaticcheckAnalyzers {
501 if !a.IsEnabled(view) {
502 opts.Analyses[a.Analyzer.Name] = true
507 func WorkspaceSymbolsString(ctx context.Context, data *Data, queryURI span.URI, symbols []protocol.SymbolInformation) (string, error) {
508 queryDir := filepath.Dir(queryURI.Filename())
509 var filtered []string
510 for _, s := range symbols {
511 uri := s.Location.URI.SpanURI()
512 dir := filepath.Dir(uri.Filename())
513 if !source.InDir(queryDir, dir) { // assume queries always issue from higher directories
516 m, err := data.Mapper(uri)
520 spn, err := m.Span(s.Location)
524 filtered = append(filtered, fmt.Sprintf("%s %s %s", spn, s.Name, s.Kind))
526 sort.Strings(filtered)
527 return strings.Join(filtered, "\n") + "\n", nil
530 func WorkspaceSymbolsTestTypeToMatcher(typ WorkspaceSymbolsTestType) source.SymbolMatcher {
532 case WorkspaceSymbolsFuzzy:
533 return source.SymbolFuzzy
534 case WorkspaceSymbolsCaseSensitive:
535 return source.SymbolCaseSensitive
537 return source.SymbolCaseInsensitive
541 func Diff(t *testing.T, want, got string) string {
545 // Add newlines to avoid newline messages in diff.
548 d, err := myers.ComputeEdits("", want, got)
552 return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d))