github.com/v2fly/tools@v0.100.0/internal/lsp/tests/util.go (about) 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. 4 5 package tests 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "go/token" 12 "path/filepath" 13 "sort" 14 "strconv" 15 "strings" 16 "testing" 17 18 "github.com/v2fly/tools/internal/lsp/diff" 19 "github.com/v2fly/tools/internal/lsp/diff/myers" 20 "github.com/v2fly/tools/internal/lsp/protocol" 21 "github.com/v2fly/tools/internal/lsp/source" 22 "github.com/v2fly/tools/internal/lsp/source/completion" 23 "github.com/v2fly/tools/internal/span" 24 ) 25 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) 35 } 36 for _, link := range gotLinks { 37 spn, err := mapper.RangeSpan(link.Range) 38 if err != nil { 39 return fmt.Sprintf("%v", err) 40 } 41 linkInNote := false 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() { 46 delete(links, spn) 47 linkInNote = true 48 } 49 } 50 if linkInNote { 51 continue 52 } 53 if target, ok := links[spn]; ok { 54 delete(links, spn) 55 if target != link.Target { 56 return fmt.Sprintf("for %v want %v, got %v\n", spn, target, link.Target) 57 } 58 } else { 59 return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target) 60 } 61 } 62 for spn, target := range links { 63 return fmt.Sprintf("missing link %v:%v\n", spn, target) 64 } 65 return "" 66 } 67 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)) 74 } 75 for i, w := range want { 76 g := got[i] 77 if w.Name != g.Name { 78 return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name) 79 } 80 if w.Kind != g.Kind { 81 return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind) 82 } 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) 85 } 86 if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" { 87 return fmt.Sprintf("children of %s: %s", w.Name, msg) 88 } 89 } 90 return "" 91 } 92 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") 96 if i >= 0 { 97 fmt.Fprintf(msg, " at %d", i) 98 } 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) 104 } 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) 108 } 109 return msg.String() 110 } 111 112 // DiffDiagnostics prints the diff between expected and actual diagnostics test 113 // results. 114 func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string { 115 source.SortDiagnostics(want) 116 source.SortDiagnostics(got) 117 118 if len(got) != len(want) { 119 return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want)) 120 } 121 for i, w := range want { 122 g := got[i] 123 if w.Message != g.Message { 124 return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message) 125 } 126 if w.Severity != g.Severity { 127 return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity) 128 } 129 if w.Source != g.Source { 130 return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source) 131 } 132 if !rangeOverlaps(g.Range, w.Range) { 133 return summarizeDiagnostics(i, uri, want, got, "range %v does not overlap %v", g.Range, w.Range) 134 } 135 } 136 return "" 137 } 138 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) { 142 return true 143 } 144 return false 145 } 146 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 149 // point). 150 func inRange(p protocol.Position, r protocol.Range) bool { 151 if protocol.IsPoint(r) { 152 return protocol.ComparePosition(r.Start, p) == 0 153 } 154 if protocol.ComparePosition(r.Start, p) <= 0 && protocol.ComparePosition(p, r.End) < 0 { 155 return true 156 } 157 return false 158 } 159 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") 163 if i >= 0 { 164 fmt.Fprintf(msg, " at %d", i) 165 } 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) 171 } 172 fmt.Fprintf(msg, "got:\n") 173 for _, d := range got { 174 fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message) 175 } 176 return msg.String() 177 } 178 179 func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string { 180 sortCodeLens(want) 181 sortCodeLens(got) 182 183 if len(got) != len(want) { 184 return summarizeCodeLens(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want)) 185 } 186 for i, w := range want { 187 g := got[i] 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) 190 } 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) 193 } 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) 196 } 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) 200 } 201 } 202 } 203 return "" 204 } 205 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 { 209 return r < 0 210 } 211 if c[i].Command.Command < c[j].Command.Command { 212 return true 213 } else if c[i].Command.Command == c[j].Command.Command { 214 return c[i].Command.Title < c[j].Command.Title 215 } else { 216 return false 217 } 218 }) 219 } 220 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") 224 if i >= 0 { 225 fmt.Fprintf(msg, " at %d", i) 226 } 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) 232 } 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) 236 } 237 return msg.String() 238 } 239 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...)) 243 } 244 if len(got.Signatures) != 1 { 245 return decorate("wanted 1 signature, got %d", len(got.Signatures)), nil 246 } 247 if got.ActiveSignature != 0 { 248 return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)), nil 249 } 250 if want.ActiveParameter != got.ActiveParameter { 251 return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)), nil 252 } 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") 258 if err != nil { 259 return "", err 260 } 261 return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d)), err 262 } 263 var paramParts []string 264 for _, p := range g.Parameters { 265 paramParts = append(paramParts, p.Label) 266 } 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 270 } 271 return "", nil 272 } 273 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 279 } 280 281 got := make(map[protocol.Location]bool) 282 for _, call := range gotCalls { 283 got[protocol.Location{URI: call.URI, Range: call.Range}] = true 284 } 285 if len(got) != len(expected) { 286 return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got)) 287 } 288 for spn := range got { 289 if !expected[spn] { 290 return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got) 291 } 292 } 293 return "" 294 } 295 296 func ToProtocolCompletionItems(items []completion.CompletionItem) []protocol.CompletionItem { 297 var result []protocol.CompletionItem 298 for _, item := range items { 299 result = append(result, ToProtocolCompletionItem(item)) 300 } 301 return result 302 } 303 304 func ToProtocolCompletionItem(item completion.CompletionItem) protocol.CompletionItem { 305 pItem := protocol.CompletionItem{ 306 Label: item.Label, 307 Kind: item.Kind, 308 Detail: item.Detail, 309 Documentation: item.Documentation, 310 InsertText: item.InsertText, 311 TextEdit: &protocol.TextEdit{ 312 NewText: item.Snippet(), 313 }, 314 // Negate score so best score has lowest sort text like real API. 315 SortText: fmt.Sprint(-item.Score), 316 } 317 if pItem.InsertText == "" { 318 pItem.InsertText = pItem.Label 319 } 320 return pItem 321 } 322 323 func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem { 324 var ( 325 got []protocol.CompletionItem 326 wantBuiltins = strings.Contains(string(src.URI()), "builtins") 327 wantKeywords = strings.Contains(string(src.URI()), "keywords") 328 ) 329 for _, item := range items { 330 if !wantBuiltins && isBuiltin(item.Label, item.Detail, item.Kind) { 331 continue 332 } 333 334 if !wantKeywords && token.Lookup(item.Label).IsKeyword() { 335 continue 336 } 337 338 got = append(got, item) 339 } 340 return got 341 } 342 343 func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool { 344 if detail == "" && kind == protocol.ClassCompletion { 345 return true 346 } 347 // Remaining builtin constants, variables, interfaces, and functions. 348 trimmed := label 349 if i := strings.Index(trimmed, "("); i >= 0 { 350 trimmed = trimmed[:i] 351 } 352 switch trimmed { 353 case "append", "cap", "close", "complex", "copy", "delete", 354 "error", "false", "imag", "iota", "len", "make", "new", 355 "nil", "panic", "print", "println", "real", "recover", "true": 356 return true 357 } 358 return false 359 } 360 361 func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string { 362 var ( 363 matchedIdxs []int 364 lastGotIdx int 365 lastGotSort float64 366 inOrder = true 367 errorMsg = "completions out of order" 368 ) 369 for _, w := range want { 370 var found bool 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) 374 found = true 375 376 if i < lastGotIdx { 377 inOrder = false 378 } 379 lastGotIdx = i 380 381 sort, _ := strconv.ParseFloat(g.SortText, 64) 382 if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort { 383 inOrder = false 384 errorMsg = "candidate scores not strictly decreasing" 385 } 386 lastGotSort = sort 387 388 break 389 } 390 } 391 if !found { 392 return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion") 393 } 394 } 395 396 sort.Ints(matchedIdxs) 397 matched := make([]protocol.CompletionItem, 0, len(matchedIdxs)) 398 for _, idx := range matchedIdxs { 399 matched = append(matched, got[idx]) 400 } 401 402 if !inOrder { 403 return summarizeCompletionItems(-1, want, matched, errorMsg) 404 } 405 406 return "" 407 } 408 409 func DiffSnippets(want string, got *protocol.CompletionItem) string { 410 if want == "" { 411 if got != nil { 412 x := got.TextEdit 413 return fmt.Sprintf("expected no snippet but got %s", x.NewText) 414 } 415 } else { 416 if got == nil { 417 return fmt.Sprintf("couldn't find completion matching %q", want) 418 } 419 x := got.TextEdit 420 if want != x.NewText { 421 return fmt.Sprintf("expected snippet %q, got %q", want, x.NewText) 422 } 423 } 424 return "" 425 } 426 427 func FindItem(list []protocol.CompletionItem, want completion.CompletionItem) *protocol.CompletionItem { 428 for _, item := range list { 429 if item.Label == want.Label { 430 return &item 431 } 432 } 433 return nil 434 } 435 436 // DiffCompletionItems prints the diff between expected and actual completion 437 // test results. 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)) 441 } 442 for i, w := range want { 443 g := got[i] 444 if w.Label != g.Label { 445 return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label) 446 } 447 if w.Detail != g.Detail { 448 return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail) 449 } 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) 453 } 454 } 455 if w.Kind != g.Kind { 456 return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind) 457 } 458 } 459 return "" 460 } 461 462 func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string { 463 msg := &bytes.Buffer{} 464 fmt.Fprint(msg, "completion failed") 465 if i >= 0 { 466 fmt.Fprintf(msg, " at %d", i) 467 } 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) 473 } 474 fmt.Fprintf(msg, "got:\n") 475 for _, d := range got { 476 fmt.Fprintf(msg, " %v\n", d) 477 } 478 return msg.String() 479 } 480 481 func EnableAllAnalyzers(view source.View, opts *source.Options) { 482 if opts.Analyses == nil { 483 opts.Analyses = make(map[string]bool) 484 } 485 for _, a := range opts.DefaultAnalyzers { 486 if !a.IsEnabled(view) { 487 opts.Analyses[a.Analyzer.Name] = true 488 } 489 } 490 for _, a := range opts.TypeErrorAnalyzers { 491 if !a.IsEnabled(view) { 492 opts.Analyses[a.Analyzer.Name] = true 493 } 494 } 495 for _, a := range opts.ConvenienceAnalyzers { 496 if !a.IsEnabled(view) { 497 opts.Analyses[a.Analyzer.Name] = true 498 } 499 } 500 for _, a := range opts.StaticcheckAnalyzers { 501 if !a.IsEnabled(view) { 502 opts.Analyses[a.Analyzer.Name] = true 503 } 504 } 505 } 506 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 514 continue 515 } 516 m, err := data.Mapper(uri) 517 if err != nil { 518 return "", err 519 } 520 spn, err := m.Span(s.Location) 521 if err != nil { 522 return "", err 523 } 524 filtered = append(filtered, fmt.Sprintf("%s %s %s", spn, s.Name, s.Kind)) 525 } 526 sort.Strings(filtered) 527 return strings.Join(filtered, "\n") + "\n", nil 528 } 529 530 func WorkspaceSymbolsTestTypeToMatcher(typ WorkspaceSymbolsTestType) source.SymbolMatcher { 531 switch typ { 532 case WorkspaceSymbolsFuzzy: 533 return source.SymbolFuzzy 534 case WorkspaceSymbolsCaseSensitive: 535 return source.SymbolCaseSensitive 536 default: 537 return source.SymbolCaseInsensitive 538 } 539 } 540 541 func Diff(t *testing.T, want, got string) string { 542 if want == got { 543 return "" 544 } 545 // Add newlines to avoid newline messages in diff. 546 want += "\n" 547 got += "\n" 548 d, err := myers.ComputeEdits("", want, got) 549 if err != nil { 550 t.Fatal(err) 551 } 552 return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d)) 553 }