github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/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/powerman/golang-tools/internal/lsp/diff" 19 "github.com/powerman/golang-tools/internal/lsp/diff/myers" 20 "github.com/powerman/golang-tools/internal/lsp/protocol" 21 "github.com/powerman/golang-tools/internal/lsp/source" 22 "github.com/powerman/golang-tools/internal/lsp/source/completion" 23 "github.com/powerman/golang-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 NormalizeAny(w.Label) != NormalizeAny(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 // NormalizeAny replaces occurrences of interface{} in input with any. 275 // 276 // In Go 1.18, standard library functions were changed to use the 'any' 277 // alias in place of interface{}, which affects their type string. 278 func NormalizeAny(input string) string { 279 return strings.ReplaceAll(input, "interface{}", "any") 280 } 281 282 // DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies 283 func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string { 284 expected := make(map[protocol.Location]bool) 285 for _, call := range expectedCalls { 286 expected[protocol.Location{URI: call.URI, Range: call.Range}] = true 287 } 288 289 got := make(map[protocol.Location]bool) 290 for _, call := range gotCalls { 291 got[protocol.Location{URI: call.URI, Range: call.Range}] = true 292 } 293 if len(got) != len(expected) { 294 return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got)) 295 } 296 for spn := range got { 297 if !expected[spn] { 298 return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got) 299 } 300 } 301 return "" 302 } 303 304 func ToProtocolCompletionItems(items []completion.CompletionItem) []protocol.CompletionItem { 305 var result []protocol.CompletionItem 306 for _, item := range items { 307 result = append(result, ToProtocolCompletionItem(item)) 308 } 309 return result 310 } 311 312 func ToProtocolCompletionItem(item completion.CompletionItem) protocol.CompletionItem { 313 pItem := protocol.CompletionItem{ 314 Label: item.Label, 315 Kind: item.Kind, 316 Detail: item.Detail, 317 Documentation: item.Documentation, 318 InsertText: item.InsertText, 319 TextEdit: &protocol.TextEdit{ 320 NewText: item.Snippet(), 321 }, 322 // Negate score so best score has lowest sort text like real API. 323 SortText: fmt.Sprint(-item.Score), 324 } 325 if pItem.InsertText == "" { 326 pItem.InsertText = pItem.Label 327 } 328 return pItem 329 } 330 331 func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem { 332 var ( 333 got []protocol.CompletionItem 334 wantBuiltins = strings.Contains(string(src.URI()), "builtins") 335 wantKeywords = strings.Contains(string(src.URI()), "keywords") 336 ) 337 for _, item := range items { 338 if !wantBuiltins && isBuiltin(item.Label, item.Detail, item.Kind) { 339 continue 340 } 341 342 if !wantKeywords && token.Lookup(item.Label).IsKeyword() { 343 continue 344 } 345 346 got = append(got, item) 347 } 348 return got 349 } 350 351 func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool { 352 if detail == "" && kind == protocol.ClassCompletion { 353 return true 354 } 355 // Remaining builtin constants, variables, interfaces, and functions. 356 trimmed := label 357 if i := strings.Index(trimmed, "("); i >= 0 { 358 trimmed = trimmed[:i] 359 } 360 switch trimmed { 361 case "append", "cap", "close", "complex", "copy", "delete", 362 "error", "false", "imag", "iota", "len", "make", "new", 363 "nil", "panic", "print", "println", "real", "recover", "true": 364 return true 365 } 366 return false 367 } 368 369 func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string { 370 var ( 371 matchedIdxs []int 372 lastGotIdx int 373 lastGotSort float64 374 inOrder = true 375 errorMsg = "completions out of order" 376 ) 377 for _, w := range want { 378 var found bool 379 for i, g := range got { 380 if w.Label == g.Label && NormalizeAny(w.Detail) == NormalizeAny(g.Detail) && w.Kind == g.Kind { 381 matchedIdxs = append(matchedIdxs, i) 382 found = true 383 384 if i < lastGotIdx { 385 inOrder = false 386 } 387 lastGotIdx = i 388 389 sort, _ := strconv.ParseFloat(g.SortText, 64) 390 if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort { 391 inOrder = false 392 errorMsg = "candidate scores not strictly decreasing" 393 } 394 lastGotSort = sort 395 396 break 397 } 398 } 399 if !found { 400 return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion") 401 } 402 } 403 404 sort.Ints(matchedIdxs) 405 matched := make([]protocol.CompletionItem, 0, len(matchedIdxs)) 406 for _, idx := range matchedIdxs { 407 matched = append(matched, got[idx]) 408 } 409 410 if !inOrder { 411 return summarizeCompletionItems(-1, want, matched, errorMsg) 412 } 413 414 return "" 415 } 416 417 func DiffSnippets(want string, got *protocol.CompletionItem) string { 418 if want == "" { 419 if got != nil { 420 x := got.TextEdit 421 return fmt.Sprintf("expected no snippet but got %s", x.NewText) 422 } 423 } else { 424 if got == nil { 425 return fmt.Sprintf("couldn't find completion matching %q", want) 426 } 427 x := got.TextEdit 428 if want != x.NewText { 429 return fmt.Sprintf("expected snippet %q, got %q", want, x.NewText) 430 } 431 } 432 return "" 433 } 434 435 func FindItem(list []protocol.CompletionItem, want completion.CompletionItem) *protocol.CompletionItem { 436 for _, item := range list { 437 if item.Label == want.Label { 438 return &item 439 } 440 } 441 return nil 442 } 443 444 // DiffCompletionItems prints the diff between expected and actual completion 445 // test results. 446 func DiffCompletionItems(want, got []protocol.CompletionItem) string { 447 if len(got) != len(want) { 448 return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want)) 449 } 450 for i, w := range want { 451 g := got[i] 452 if w.Label != g.Label { 453 return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label) 454 } 455 if NormalizeAny(w.Detail) != NormalizeAny(g.Detail) { 456 return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail) 457 } 458 if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") { 459 if w.Documentation != g.Documentation { 460 return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation) 461 } 462 } 463 if w.Kind != g.Kind { 464 return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind) 465 } 466 } 467 return "" 468 } 469 470 func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string { 471 msg := &bytes.Buffer{} 472 fmt.Fprint(msg, "completion failed") 473 if i >= 0 { 474 fmt.Fprintf(msg, " at %d", i) 475 } 476 fmt.Fprint(msg, " because of ") 477 fmt.Fprintf(msg, reason, args...) 478 fmt.Fprint(msg, ":\nexpected:\n") 479 for _, d := range want { 480 fmt.Fprintf(msg, " %v\n", d) 481 } 482 fmt.Fprintf(msg, "got:\n") 483 for _, d := range got { 484 fmt.Fprintf(msg, " %v\n", d) 485 } 486 return msg.String() 487 } 488 489 func EnableAllAnalyzers(view source.View, opts *source.Options) { 490 if opts.Analyses == nil { 491 opts.Analyses = make(map[string]bool) 492 } 493 for _, a := range opts.DefaultAnalyzers { 494 if !a.IsEnabled(view) { 495 opts.Analyses[a.Analyzer.Name] = true 496 } 497 } 498 for _, a := range opts.TypeErrorAnalyzers { 499 if !a.IsEnabled(view) { 500 opts.Analyses[a.Analyzer.Name] = true 501 } 502 } 503 for _, a := range opts.ConvenienceAnalyzers { 504 if !a.IsEnabled(view) { 505 opts.Analyses[a.Analyzer.Name] = true 506 } 507 } 508 for _, a := range opts.StaticcheckAnalyzers { 509 if !a.IsEnabled(view) { 510 opts.Analyses[a.Analyzer.Name] = true 511 } 512 } 513 } 514 515 func WorkspaceSymbolsString(ctx context.Context, data *Data, queryURI span.URI, symbols []protocol.SymbolInformation) (string, error) { 516 queryDir := filepath.Dir(queryURI.Filename()) 517 var filtered []string 518 for _, s := range symbols { 519 uri := s.Location.URI.SpanURI() 520 dir := filepath.Dir(uri.Filename()) 521 if !source.InDir(queryDir, dir) { // assume queries always issue from higher directories 522 continue 523 } 524 m, err := data.Mapper(uri) 525 if err != nil { 526 return "", err 527 } 528 spn, err := m.Span(s.Location) 529 if err != nil { 530 return "", err 531 } 532 filtered = append(filtered, fmt.Sprintf("%s %s %s", spn, s.Name, s.Kind)) 533 } 534 sort.Strings(filtered) 535 return strings.Join(filtered, "\n") + "\n", nil 536 } 537 538 func WorkspaceSymbolsTestTypeToMatcher(typ WorkspaceSymbolsTestType) source.SymbolMatcher { 539 switch typ { 540 case WorkspaceSymbolsFuzzy: 541 return source.SymbolFuzzy 542 case WorkspaceSymbolsCaseSensitive: 543 return source.SymbolCaseSensitive 544 default: 545 return source.SymbolCaseInsensitive 546 } 547 } 548 549 func Diff(t *testing.T, want, got string) string { 550 if want == got { 551 return "" 552 } 553 // Add newlines to avoid newline messages in diff. 554 want += "\n" 555 got += "\n" 556 d, err := myers.ComputeEdits("", want, got) 557 if err != nil { 558 t.Fatal(err) 559 } 560 return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d)) 561 } 562 563 // StripSubscripts removes type parameter id subscripts. 564 // 565 // TODO(rfindley): remove this function once subscripts are removed from the 566 // type parameter type string. 567 func StripSubscripts(s string) string { 568 var runes []rune 569 for _, r := range s { 570 // For debugging/uniqueness purposes, TypeString on a type parameter adds a 571 // subscript corresponding to the type parameter's unique id. This is going 572 // to be removed, but in the meantime we skip the subscript runes to get a 573 // deterministic output. 574 if '₀' <= r && r < '₀'+10 { 575 continue // trim type parameter subscripts 576 } 577 runes = append(runes, r) 578 } 579 return string(runes) 580 }