github.com/jd-ly/tools@v0.5.7/internal/lsp/lsp_test.go (about) 1 // Copyright 2018 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 lsp 6 7 import ( 8 "context" 9 "fmt" 10 "go/token" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "sort" 15 "strings" 16 "testing" 17 18 "github.com/jd-ly/tools/internal/lsp/cache" 19 "github.com/jd-ly/tools/internal/lsp/diff" 20 "github.com/jd-ly/tools/internal/lsp/diff/myers" 21 "github.com/jd-ly/tools/internal/lsp/protocol" 22 "github.com/jd-ly/tools/internal/lsp/source" 23 "github.com/jd-ly/tools/internal/lsp/tests" 24 "github.com/jd-ly/tools/internal/span" 25 "github.com/jd-ly/tools/internal/testenv" 26 ) 27 28 func TestMain(m *testing.M) { 29 testenv.ExitIfSmallMachine() 30 os.Exit(m.Run()) 31 } 32 33 func TestLSP(t *testing.T) { 34 tests.RunTests(t, "testdata", true, testLSP) 35 } 36 37 type runner struct { 38 server *Server 39 data *tests.Data 40 diagnostics map[span.URI][]*source.Diagnostic 41 ctx context.Context 42 normalizers []tests.Normalizer 43 } 44 45 func testLSP(t *testing.T, datum *tests.Data) { 46 ctx := tests.Context(t) 47 48 cache := cache.New(ctx, nil) 49 session := cache.NewSession(ctx) 50 options := source.DefaultOptions().Clone() 51 tests.DefaultOptions(options) 52 session.SetOptions(options) 53 options.SetEnvSlice(datum.Config.Env) 54 view, snapshot, release, err := session.NewView(ctx, datum.Config.Dir, span.URIFromPath(datum.Config.Dir), "", options) 55 if err != nil { 56 t.Fatal(err) 57 } 58 59 defer view.Shutdown(ctx) 60 61 // Enable type error analyses for tests. 62 // TODO(golang/go#38212): Delete this once they are enabled by default. 63 tests.EnableAllAnalyzers(view, options) 64 view.SetOptions(ctx, options) 65 66 // Only run the -modfile specific tests in module mode with Go 1.14 or above. 67 datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14 68 release() 69 70 var modifications []source.FileModification 71 for filename, content := range datum.Config.Overlay { 72 kind := source.DetectLanguage("", filename) 73 if kind != source.Go { 74 continue 75 } 76 modifications = append(modifications, source.FileModification{ 77 URI: span.URIFromPath(filename), 78 Action: source.Open, 79 Version: -1, 80 Text: content, 81 LanguageID: "go", 82 }) 83 } 84 if err := session.ModifyFiles(ctx, modifications); err != nil { 85 t.Fatal(err) 86 } 87 r := &runner{ 88 server: NewServer(session, testClient{}), 89 data: datum, 90 ctx: ctx, 91 normalizers: tests.CollectNormalizers(datum.Exported), 92 } 93 tests.Run(t, r, datum) 94 } 95 96 // testClient stubs any client functions that may be called by LSP functions. 97 type testClient struct { 98 protocol.Client 99 } 100 101 // Trivially implement PublishDiagnostics so that we can call 102 // server.publishReports below to de-dup sent diagnostics. 103 func (c testClient) PublishDiagnostics(context.Context, *protocol.PublishDiagnosticsParams) error { 104 return nil 105 } 106 107 func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) { 108 mapper, err := r.data.Mapper(spn.URI()) 109 if err != nil { 110 t.Fatal(err) 111 } 112 loc, err := mapper.Location(spn) 113 if err != nil { 114 t.Fatalf("failed for %v: %v", spn, err) 115 } 116 117 params := &protocol.CallHierarchyPrepareParams{ 118 TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 119 TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, 120 Position: loc.Range.Start, 121 }, 122 } 123 124 items, err := r.server.PrepareCallHierarchy(r.ctx, params) 125 if err != nil { 126 t.Fatal(err) 127 } 128 if len(items) == 0 { 129 t.Fatalf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range) 130 } 131 132 callLocation := protocol.Location{ 133 URI: items[0].URI, 134 Range: items[0].Range, 135 } 136 if callLocation != loc { 137 t.Fatalf("expected server.PrepareCallHierarchy to return identifier at %v but got %v\n", loc, callLocation) 138 } 139 140 incomingCalls, err := r.server.IncomingCalls(r.ctx, &protocol.CallHierarchyIncomingCallsParams{Item: items[0]}) 141 if err != nil { 142 t.Error(err) 143 } 144 var incomingCallItems []protocol.CallHierarchyItem 145 for _, item := range incomingCalls { 146 incomingCallItems = append(incomingCallItems, item.From) 147 } 148 msg := tests.DiffCallHierarchyItems(incomingCallItems, expectedCalls.IncomingCalls) 149 if msg != "" { 150 t.Error(fmt.Sprintf("incoming calls: %s", msg)) 151 } 152 153 outgoingCalls, err := r.server.OutgoingCalls(r.ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: items[0]}) 154 if err != nil { 155 t.Error(err) 156 } 157 var outgoingCallItems []protocol.CallHierarchyItem 158 for _, item := range outgoingCalls { 159 outgoingCallItems = append(outgoingCallItems, item.To) 160 } 161 msg = tests.DiffCallHierarchyItems(outgoingCallItems, expectedCalls.OutgoingCalls) 162 if msg != "" { 163 t.Error(fmt.Sprintf("outgoing calls: %s", msg)) 164 } 165 } 166 167 func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) { 168 if source.DetectLanguage("", uri.Filename()) != source.Mod { 169 return 170 } 171 got, err := r.server.codeLens(r.ctx, &protocol.CodeLensParams{ 172 TextDocument: protocol.TextDocumentIdentifier{ 173 URI: protocol.DocumentURI(uri), 174 }, 175 }) 176 if err != nil { 177 t.Fatal(err) 178 } 179 if diff := tests.DiffCodeLens(uri, want, got); diff != "" { 180 t.Errorf("%s: %s", uri, diff) 181 } 182 } 183 184 func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) { 185 // Get the diagnostics for this view if we have not done it before. 186 v := r.server.session.View(r.data.Config.Dir) 187 r.collectDiagnostics(v) 188 d := r.diagnostics[uri] 189 got := make([]*source.Diagnostic, len(d)) 190 copy(got, d) 191 // A special case to test that there are no diagnostics for a file. 192 if len(want) == 1 && want[0].Source == "no_diagnostics" { 193 if len(got) != 0 { 194 t.Errorf("expected no diagnostics for %s, got %v", uri, got) 195 } 196 return 197 } 198 if diff := tests.DiffDiagnostics(uri, want, got); diff != "" { 199 t.Error(diff) 200 } 201 } 202 203 func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { 204 uri := spn.URI() 205 view, err := r.server.session.ViewOf(uri) 206 if err != nil { 207 t.Fatal(err) 208 } 209 original := view.Options() 210 modified := original 211 212 // Test all folding ranges. 213 modified.LineFoldingOnly = false 214 view, err = view.SetOptions(r.ctx, modified) 215 if err != nil { 216 t.Error(err) 217 return 218 } 219 ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ 220 TextDocument: protocol.TextDocumentIdentifier{ 221 URI: protocol.URIFromSpanURI(uri), 222 }, 223 }) 224 if err != nil { 225 t.Error(err) 226 return 227 } 228 r.foldingRanges(t, "foldingRange", uri, ranges) 229 230 // Test folding ranges with lineFoldingOnly = true. 231 modified.LineFoldingOnly = true 232 view, err = view.SetOptions(r.ctx, modified) 233 if err != nil { 234 t.Error(err) 235 return 236 } 237 ranges, err = r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ 238 TextDocument: protocol.TextDocumentIdentifier{ 239 URI: protocol.URIFromSpanURI(uri), 240 }, 241 }) 242 if err != nil { 243 t.Error(err) 244 return 245 } 246 r.foldingRanges(t, "foldingRange-lineFolding", uri, ranges) 247 view.SetOptions(r.ctx, original) 248 } 249 250 func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) { 251 m, err := r.data.Mapper(uri) 252 if err != nil { 253 t.Fatal(err) 254 } 255 // Fold all ranges. 256 nonOverlapping := nonOverlappingRanges(ranges) 257 for i, rngs := range nonOverlapping { 258 got, err := foldRanges(m, string(m.Content), rngs) 259 if err != nil { 260 t.Error(err) 261 continue 262 } 263 tag := fmt.Sprintf("%s-%d", prefix, i) 264 want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { 265 return []byte(got), nil 266 })) 267 268 if want != got { 269 t.Errorf("%s: foldingRanges failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got) 270 } 271 } 272 273 // Filter by kind. 274 kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment} 275 for _, kind := range kinds { 276 var kindOnly []protocol.FoldingRange 277 for _, fRng := range ranges { 278 if fRng.Kind == string(kind) { 279 kindOnly = append(kindOnly, fRng) 280 } 281 } 282 283 nonOverlapping := nonOverlappingRanges(kindOnly) 284 for i, rngs := range nonOverlapping { 285 got, err := foldRanges(m, string(m.Content), rngs) 286 if err != nil { 287 t.Error(err) 288 continue 289 } 290 tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i) 291 want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { 292 return []byte(got), nil 293 })) 294 295 if want != got { 296 t.Errorf("%s: foldingRanges failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got) 297 } 298 } 299 300 } 301 } 302 303 func nonOverlappingRanges(ranges []protocol.FoldingRange) (res [][]protocol.FoldingRange) { 304 for _, fRng := range ranges { 305 setNum := len(res) 306 for i := 0; i < len(res); i++ { 307 canInsert := true 308 for _, rng := range res[i] { 309 if conflict(rng, fRng) { 310 canInsert = false 311 break 312 } 313 } 314 if canInsert { 315 setNum = i 316 break 317 } 318 } 319 if setNum == len(res) { 320 res = append(res, []protocol.FoldingRange{}) 321 } 322 res[setNum] = append(res[setNum], fRng) 323 } 324 return res 325 } 326 327 func conflict(a, b protocol.FoldingRange) bool { 328 // a start position is <= b start positions 329 return (a.StartLine < b.StartLine || (a.StartLine == b.StartLine && a.StartCharacter <= b.StartCharacter)) && 330 (a.EndLine > b.StartLine || (a.EndLine == b.StartLine && a.EndCharacter > b.StartCharacter)) 331 } 332 333 func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.FoldingRange) (string, error) { 334 foldedText := "<>" 335 res := contents 336 // Apply the edits from the end of the file forward 337 // to preserve the offsets 338 for i := len(ranges) - 1; i >= 0; i-- { 339 fRange := ranges[i] 340 spn, err := m.RangeSpan(protocol.Range{ 341 Start: protocol.Position{ 342 Line: fRange.StartLine, 343 Character: fRange.StartCharacter, 344 }, 345 End: protocol.Position{ 346 Line: fRange.EndLine, 347 Character: fRange.EndCharacter, 348 }, 349 }) 350 if err != nil { 351 return "", err 352 } 353 start := spn.Start().Offset() 354 end := spn.End().Offset() 355 356 tmp := res[0:start] + foldedText 357 res = tmp + res[end:] 358 } 359 return res, nil 360 } 361 362 func (r *runner) Format(t *testing.T, spn span.Span) { 363 uri := spn.URI() 364 filename := uri.Filename() 365 gofmted := string(r.data.Golden("gofmt", filename, func() ([]byte, error) { 366 cmd := exec.Command("gofmt", filename) 367 out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files 368 return out, nil 369 })) 370 371 edits, err := r.server.Formatting(r.ctx, &protocol.DocumentFormattingParams{ 372 TextDocument: protocol.TextDocumentIdentifier{ 373 URI: protocol.URIFromSpanURI(uri), 374 }, 375 }) 376 if err != nil { 377 if gofmted != "" { 378 t.Error(err) 379 } 380 return 381 } 382 m, err := r.data.Mapper(uri) 383 if err != nil { 384 t.Fatal(err) 385 } 386 sedits, err := source.FromProtocolEdits(m, edits) 387 if err != nil { 388 t.Error(err) 389 } 390 got := diff.ApplyEdits(string(m.Content), sedits) 391 if gofmted != got { 392 t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got) 393 } 394 } 395 396 func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { 397 uri := spn.URI() 398 filename := uri.Filename() 399 // this is called solely for coverage in semantic.go 400 _, err := r.server.semanticTokensFull(r.ctx, &protocol.SemanticTokensParams{ 401 TextDocument: protocol.TextDocumentIdentifier{ 402 URI: protocol.URIFromSpanURI(uri), 403 }, 404 }) 405 if err != nil { 406 t.Errorf("%v for %s", err, filename) 407 } 408 _, err = r.server.semanticTokensRange(r.ctx, &protocol.SemanticTokensRangeParams{ 409 TextDocument: protocol.TextDocumentIdentifier{ 410 URI: protocol.URIFromSpanURI(uri), 411 }, 412 // any legal range. Just to exercise the call. 413 Range: protocol.Range{ 414 Start: protocol.Position{ 415 Line: 0, 416 Character: 0, 417 }, 418 End: protocol.Position{ 419 Line: 2, 420 Character: 0, 421 }, 422 }, 423 }) 424 if err != nil { 425 t.Errorf("%v for Range %s", err, filename) 426 } 427 } 428 429 func (r *runner) Import(t *testing.T, spn span.Span) { 430 uri := spn.URI() 431 filename := uri.Filename() 432 actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ 433 TextDocument: protocol.TextDocumentIdentifier{ 434 URI: protocol.URIFromSpanURI(uri), 435 }, 436 }) 437 if err != nil { 438 t.Fatal(err) 439 } 440 m, err := r.data.Mapper(uri) 441 if err != nil { 442 t.Fatal(err) 443 } 444 got := string(m.Content) 445 if len(actions) > 0 { 446 res, err := applyTextDocumentEdits(r, actions[0].Edit.DocumentChanges) 447 if err != nil { 448 t.Fatal(err) 449 } 450 got = res[uri] 451 } 452 want := string(r.data.Golden("goimports", filename, func() ([]byte, error) { 453 return []byte(got), nil 454 })) 455 if want != got { 456 d := myers.ComputeEdits(uri, want, got) 457 t.Errorf("import failed for %s: %s", filename, diff.ToUnified("want", "got", want, d)) 458 } 459 } 460 461 func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, expectedActions int) { 462 uri := spn.URI() 463 view, err := r.server.session.ViewOf(uri) 464 if err != nil { 465 t.Fatal(err) 466 } 467 468 snapshot, release := view.Snapshot(r.ctx) 469 defer release() 470 471 fh, err := snapshot.GetVersionedFile(r.ctx, uri) 472 if err != nil { 473 t.Fatal(err) 474 } 475 m, err := r.data.Mapper(uri) 476 if err != nil { 477 t.Fatal(err) 478 } 479 rng, err := m.Range(spn) 480 if err != nil { 481 t.Fatal(err) 482 } 483 // Get the diagnostics for this view if we have not done it before. 484 r.collectDiagnostics(view) 485 var diagnostics []protocol.Diagnostic 486 for _, d := range r.diagnostics[uri] { 487 // Compare the start positions rather than the entire range because 488 // some diagnostics have a range with the same start and end position (8:1-8:1). 489 // The current marker functionality prevents us from having a range of 0 length. 490 if protocol.ComparePosition(d.Range.Start, rng.Start) == 0 { 491 diagnostics = append(diagnostics, toProtocolDiagnostics([]*source.Diagnostic{d})...) 492 break 493 } 494 } 495 codeActionKinds := []protocol.CodeActionKind{} 496 for _, k := range actionKinds { 497 codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k)) 498 } 499 actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ 500 TextDocument: protocol.TextDocumentIdentifier{ 501 URI: protocol.URIFromSpanURI(uri), 502 }, 503 Range: rng, 504 Context: protocol.CodeActionContext{ 505 Only: codeActionKinds, 506 Diagnostics: diagnostics, 507 }, 508 }) 509 if err != nil { 510 t.Fatalf("CodeAction %s failed: %v", spn, err) 511 } 512 if len(actions) != expectedActions { 513 // Hack: We assume that we only get one code action per range. 514 var cmds []string 515 for _, a := range actions { 516 cmds = append(cmds, fmt.Sprintf("%s (%s)", a.Command.Command, a.Title)) 517 } 518 t.Fatalf("unexpected number of code actions, want %d, got %d: %v", expectedActions, len(actions), cmds) 519 } 520 action := actions[0] 521 var match bool 522 for _, k := range codeActionKinds { 523 if action.Kind == k { 524 match = true 525 break 526 } 527 } 528 if !match { 529 t.Fatalf("unexpected kind for code action %s, expected one of %v, got %v", action.Title, codeActionKinds, action.Kind) 530 } 531 var res map[span.URI]string 532 if cmd := action.Command; cmd != nil { 533 edits, err := commandToEdits(r.ctx, snapshot, fh, rng, action.Command.Command) 534 if err != nil { 535 t.Fatalf("error converting command %q to edits: %v", action.Command.Command, err) 536 } 537 res, err = applyTextDocumentEdits(r, edits) 538 if err != nil { 539 t.Fatal(err) 540 } 541 } else { 542 res, err = applyTextDocumentEdits(r, action.Edit.DocumentChanges) 543 if err != nil { 544 t.Fatal(err) 545 } 546 } 547 for u, got := range res { 548 want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { 549 return []byte(got), nil 550 })) 551 if want != got { 552 t.Errorf("suggested fixes failed for %s:\n%s", u.Filename(), tests.Diff(want, got)) 553 } 554 } 555 } 556 557 func commandToEdits(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, rng protocol.Range, cmd string) ([]protocol.TextDocumentEdit, error) { 558 var command *source.Command 559 for _, c := range source.Commands { 560 if c.ID() == cmd { 561 command = c 562 break 563 } 564 } 565 if command == nil { 566 return nil, fmt.Errorf("no known command for %s", cmd) 567 } 568 if !command.Applies(ctx, snapshot, fh, rng) { 569 return nil, fmt.Errorf("cannot apply %v", command.ID()) 570 } 571 edits, err := command.SuggestedFix(ctx, snapshot, fh, rng) 572 if err != nil { 573 return nil, fmt.Errorf("error calling command.SuggestedFix: %v", err) 574 } 575 return edits, nil 576 } 577 578 func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) { 579 uri := start.URI() 580 view, err := r.server.session.ViewOf(uri) 581 if err != nil { 582 t.Fatal(err) 583 } 584 585 snapshot, release := view.Snapshot(r.ctx) 586 defer release() 587 588 fh, err := snapshot.GetVersionedFile(r.ctx, uri) 589 if err != nil { 590 t.Fatal(err) 591 } 592 m, err := r.data.Mapper(uri) 593 if err != nil { 594 t.Fatal(err) 595 } 596 spn := span.New(start.URI(), start.Start(), end.End()) 597 rng, err := m.Range(spn) 598 if err != nil { 599 t.Fatal(err) 600 } 601 actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ 602 TextDocument: protocol.TextDocumentIdentifier{ 603 URI: protocol.URIFromSpanURI(uri), 604 }, 605 Range: rng, 606 Context: protocol.CodeActionContext{ 607 Only: []protocol.CodeActionKind{"refactor.extract"}, 608 }, 609 }) 610 if err != nil { 611 t.Fatal(err) 612 } 613 // Hack: We assume that we only get one code action per range. 614 // TODO(rstambler): Support multiple code actions per test. 615 if len(actions) == 0 || len(actions) > 1 { 616 t.Fatalf("unexpected number of code actions, want 1, got %v", len(actions)) 617 } 618 edits, err := commandToEdits(r.ctx, snapshot, fh, rng, actions[0].Command.Command) 619 if err != nil { 620 t.Fatal(err) 621 } 622 res, err := applyTextDocumentEdits(r, edits) 623 if err != nil { 624 t.Fatal(err) 625 } 626 for u, got := range res { 627 want := string(r.data.Golden("functionextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { 628 return []byte(got), nil 629 })) 630 if want != got { 631 t.Errorf("function extraction failed for %s:\n%s", u.Filename(), tests.Diff(want, got)) 632 } 633 } 634 } 635 636 func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { 637 sm, err := r.data.Mapper(d.Src.URI()) 638 if err != nil { 639 t.Fatal(err) 640 } 641 loc, err := sm.Location(d.Src) 642 if err != nil { 643 t.Fatalf("failed for %v: %v", d.Src, err) 644 } 645 tdpp := protocol.TextDocumentPositionParams{ 646 TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, 647 Position: loc.Range.Start, 648 } 649 var locs []protocol.Location 650 var hover *protocol.Hover 651 if d.IsType { 652 params := &protocol.TypeDefinitionParams{ 653 TextDocumentPositionParams: tdpp, 654 } 655 locs, err = r.server.TypeDefinition(r.ctx, params) 656 } else { 657 params := &protocol.DefinitionParams{ 658 TextDocumentPositionParams: tdpp, 659 } 660 locs, err = r.server.Definition(r.ctx, params) 661 if err != nil { 662 t.Fatalf("failed for %v: %+v", d.Src, err) 663 } 664 v := &protocol.HoverParams{ 665 TextDocumentPositionParams: tdpp, 666 } 667 hover, err = r.server.Hover(r.ctx, v) 668 } 669 if err != nil { 670 t.Fatalf("failed for %v: %v", d.Src, err) 671 } 672 if len(locs) != 1 { 673 t.Errorf("got %d locations for definition, expected 1", len(locs)) 674 } 675 didSomething := false 676 if hover != nil { 677 didSomething = true 678 tag := fmt.Sprintf("%s-hover", d.Name) 679 expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) { 680 return []byte(hover.Contents.Value), nil 681 })) 682 if hover.Contents.Value != expectHover { 683 t.Errorf("%s:\n%s", d.Src, tests.Diff(expectHover, hover.Contents.Value)) 684 } 685 } 686 if !d.OnlyHover { 687 didSomething = true 688 locURI := locs[0].URI.SpanURI() 689 lm, err := r.data.Mapper(locURI) 690 if err != nil { 691 t.Fatal(err) 692 } 693 if def, err := lm.Span(locs[0]); err != nil { 694 t.Fatalf("failed for %v: %v", locs[0], err) 695 } else if def != d.Def { 696 t.Errorf("for %v got %v want %v", d.Src, def, d.Def) 697 } 698 } 699 if !didSomething { 700 t.Errorf("no tests ran for %s", d.Src.URI()) 701 } 702 } 703 704 func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) { 705 sm, err := r.data.Mapper(spn.URI()) 706 if err != nil { 707 t.Fatal(err) 708 } 709 loc, err := sm.Location(spn) 710 if err != nil { 711 t.Fatalf("failed for %v: %v", spn, err) 712 } 713 tdpp := protocol.TextDocumentPositionParams{ 714 TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, 715 Position: loc.Range.Start, 716 } 717 var locs []protocol.Location 718 params := &protocol.ImplementationParams{ 719 TextDocumentPositionParams: tdpp, 720 } 721 locs, err = r.server.Implementation(r.ctx, params) 722 if err != nil { 723 t.Fatalf("failed for %v: %v", spn, err) 724 } 725 if len(locs) != len(impls) { 726 t.Fatalf("got %d locations for implementation, expected %d", len(locs), len(impls)) 727 } 728 729 var results []span.Span 730 for i := range locs { 731 locURI := locs[i].URI.SpanURI() 732 lm, err := r.data.Mapper(locURI) 733 if err != nil { 734 t.Fatal(err) 735 } 736 imp, err := lm.Span(locs[i]) 737 if err != nil { 738 t.Fatalf("failed for %v: %v", locs[i], err) 739 } 740 results = append(results, imp) 741 } 742 // Sort results and expected to make tests deterministic. 743 sort.SliceStable(results, func(i, j int) bool { 744 return span.Compare(results[i], results[j]) == -1 745 }) 746 sort.SliceStable(impls, func(i, j int) bool { 747 return span.Compare(impls[i], impls[j]) == -1 748 }) 749 for i := range results { 750 if results[i] != impls[i] { 751 t.Errorf("for %dth implementation of %v got %v want %v", i, spn, results[i], impls[i]) 752 } 753 } 754 } 755 756 func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { 757 m, err := r.data.Mapper(src.URI()) 758 if err != nil { 759 t.Fatal(err) 760 } 761 loc, err := m.Location(src) 762 if err != nil { 763 t.Fatalf("failed for %v: %v", locations[0], err) 764 } 765 tdpp := protocol.TextDocumentPositionParams{ 766 TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, 767 Position: loc.Range.Start, 768 } 769 params := &protocol.DocumentHighlightParams{ 770 TextDocumentPositionParams: tdpp, 771 } 772 highlights, err := r.server.DocumentHighlight(r.ctx, params) 773 if err != nil { 774 t.Fatal(err) 775 } 776 if len(highlights) != len(locations) { 777 t.Fatalf("got %d highlights for highlight at %v:%v:%v, expected %d", len(highlights), src.URI().Filename(), src.Start().Line(), src.Start().Column(), len(locations)) 778 } 779 // Check to make sure highlights have a valid range. 780 var results []span.Span 781 for i := range highlights { 782 h, err := m.RangeSpan(highlights[i].Range) 783 if err != nil { 784 t.Fatalf("failed for %v: %v", highlights[i], err) 785 } 786 results = append(results, h) 787 } 788 // Sort results to make tests deterministic since DocumentHighlight uses a map. 789 sort.SliceStable(results, func(i, j int) bool { 790 return span.Compare(results[i], results[j]) == -1 791 }) 792 // Check to make sure all the expected highlights are found. 793 for i := range results { 794 if results[i] != locations[i] { 795 t.Errorf("want %v, got %v\n", locations[i], results[i]) 796 } 797 } 798 } 799 800 func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { 801 sm, err := r.data.Mapper(src.URI()) 802 if err != nil { 803 t.Fatal(err) 804 } 805 loc, err := sm.Location(src) 806 if err != nil { 807 t.Fatalf("failed for %v: %v", src, err) 808 } 809 for _, includeDeclaration := range []bool{true, false} { 810 t.Run(fmt.Sprintf("refs-declaration-%v", includeDeclaration), func(t *testing.T) { 811 want := make(map[protocol.Location]bool) 812 for i, pos := range itemList { 813 // We don't want the first result if we aren't including the declaration. 814 if i == 0 && !includeDeclaration { 815 continue 816 } 817 m, err := r.data.Mapper(pos.URI()) 818 if err != nil { 819 t.Fatal(err) 820 } 821 loc, err := m.Location(pos) 822 if err != nil { 823 t.Fatalf("failed for %v: %v", src, err) 824 } 825 want[loc] = true 826 } 827 params := &protocol.ReferenceParams{ 828 TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 829 TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, 830 Position: loc.Range.Start, 831 }, 832 Context: protocol.ReferenceContext{ 833 IncludeDeclaration: includeDeclaration, 834 }, 835 } 836 got, err := r.server.References(r.ctx, params) 837 if err != nil { 838 t.Fatalf("failed for %v: %v", src, err) 839 } 840 if len(got) != len(want) { 841 t.Errorf("references failed: different lengths got %v want %v", len(got), len(want)) 842 } 843 for _, loc := range got { 844 if !want[loc] { 845 t.Errorf("references failed: incorrect references got %v want %v", loc, want) 846 } 847 } 848 }) 849 } 850 } 851 852 func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { 853 tag := fmt.Sprintf("%s-rename", newText) 854 855 uri := spn.URI() 856 filename := uri.Filename() 857 sm, err := r.data.Mapper(uri) 858 if err != nil { 859 t.Fatal(err) 860 } 861 loc, err := sm.Location(spn) 862 if err != nil { 863 t.Fatalf("failed for %v: %v", spn, err) 864 } 865 866 wedit, err := r.server.Rename(r.ctx, &protocol.RenameParams{ 867 TextDocument: protocol.TextDocumentIdentifier{ 868 URI: protocol.URIFromSpanURI(uri), 869 }, 870 Position: loc.Range.Start, 871 NewName: newText, 872 }) 873 if err != nil { 874 renamed := string(r.data.Golden(tag, filename, func() ([]byte, error) { 875 return []byte(err.Error()), nil 876 })) 877 if err.Error() != renamed { 878 t.Errorf("rename failed for %s, expected:\n%v\ngot:\n%v\n", newText, renamed, err) 879 } 880 return 881 } 882 res, err := applyTextDocumentEdits(r, wedit.DocumentChanges) 883 if err != nil { 884 t.Fatal(err) 885 } 886 var orderedURIs []string 887 for uri := range res { 888 orderedURIs = append(orderedURIs, string(uri)) 889 } 890 sort.Strings(orderedURIs) 891 892 var got string 893 for i := 0; i < len(res); i++ { 894 if i != 0 { 895 got += "\n" 896 } 897 uri := span.URIFromURI(orderedURIs[i]) 898 if len(res) > 1 { 899 got += filepath.Base(uri.Filename()) + ":\n" 900 } 901 val := res[uri] 902 got += val 903 } 904 want := string(r.data.Golden(tag, filename, func() ([]byte, error) { 905 return []byte(got), nil 906 })) 907 if want != got { 908 t.Errorf("rename failed for %s:\n%s", newText, tests.Diff(want, got)) 909 } 910 } 911 912 func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { 913 m, err := r.data.Mapper(src.URI()) 914 if err != nil { 915 t.Fatal(err) 916 } 917 loc, err := m.Location(src) 918 if err != nil { 919 t.Fatalf("failed for %v: %v", src, err) 920 } 921 tdpp := protocol.TextDocumentPositionParams{ 922 TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, 923 Position: loc.Range.Start, 924 } 925 params := &protocol.PrepareRenameParams{ 926 TextDocumentPositionParams: tdpp, 927 } 928 got, err := r.server.PrepareRename(context.Background(), params) 929 if err != nil { 930 t.Errorf("prepare rename failed for %v: got error: %v", src, err) 931 return 932 } 933 // we all love typed nils 934 if got == nil { 935 if want.Text != "" { // expected an ident. 936 t.Errorf("prepare rename failed for %v: got nil", src) 937 } 938 return 939 } 940 if got.Start == got.End { 941 // Special case for 0-length ranges. Marks can't specify a 0-length range, 942 // so just compare the start. 943 if got.Start != want.Range.Start { 944 t.Errorf("prepare rename failed: incorrect point, got %v want %v", got.Start, want.Range.Start) 945 } 946 } else { 947 if protocol.CompareRange(*got, want.Range) != 0 { 948 t.Errorf("prepare rename failed: incorrect range got %v want %v", *got, want.Range) 949 } 950 } 951 } 952 953 func applyTextDocumentEdits(r *runner, edits []protocol.TextDocumentEdit) (map[span.URI]string, error) { 954 res := map[span.URI]string{} 955 for _, docEdits := range edits { 956 uri := docEdits.TextDocument.URI.SpanURI() 957 var m *protocol.ColumnMapper 958 // If we have already edited this file, we use the edited version (rather than the 959 // file in its original state) so that we preserve our initial changes. 960 if content, ok := res[uri]; ok { 961 m = &protocol.ColumnMapper{ 962 URI: uri, 963 Converter: span.NewContentConverter( 964 uri.Filename(), []byte(content)), 965 Content: []byte(content), 966 } 967 } else { 968 var err error 969 if m, err = r.data.Mapper(uri); err != nil { 970 return nil, err 971 } 972 } 973 res[uri] = string(m.Content) 974 sedits, err := source.FromProtocolEdits(m, docEdits.Edits) 975 if err != nil { 976 return nil, err 977 } 978 res[uri] = applyEdits(res[uri], sedits) 979 } 980 return res, nil 981 } 982 983 func applyEdits(contents string, edits []diff.TextEdit) string { 984 res := contents 985 986 // Apply the edits from the end of the file forward 987 // to preserve the offsets 988 for i := len(edits) - 1; i >= 0; i-- { 989 edit := edits[i] 990 start := edit.Span.Start().Offset() 991 end := edit.Span.End().Offset() 992 tmp := res[0:start] + edit.NewText 993 res = tmp + res[end:] 994 } 995 return res 996 } 997 998 func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { 999 params := &protocol.DocumentSymbolParams{ 1000 TextDocument: protocol.TextDocumentIdentifier{ 1001 URI: protocol.URIFromSpanURI(uri), 1002 }, 1003 } 1004 got, err := r.server.DocumentSymbol(r.ctx, params) 1005 if err != nil { 1006 t.Fatal(err) 1007 } 1008 if len(got) != len(expectedSymbols) { 1009 t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(got)) 1010 return 1011 } 1012 symbols := make([]protocol.DocumentSymbol, len(got)) 1013 for i, s := range got { 1014 s, ok := s.(protocol.DocumentSymbol) 1015 if !ok { 1016 t.Fatalf("%v: wanted []DocumentSymbols but got %v", uri, got) 1017 } 1018 symbols[i] = s 1019 } 1020 if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" { 1021 t.Error(diff) 1022 } 1023 } 1024 1025 func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { 1026 r.callWorkspaceSymbols(t, uri, query, typ) 1027 } 1028 1029 func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { 1030 t.Helper() 1031 1032 matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ) 1033 1034 original := r.server.session.Options() 1035 modified := original 1036 modified.SymbolMatcher = matcher 1037 r.server.session.SetOptions(modified) 1038 defer r.server.session.SetOptions(original) 1039 1040 params := &protocol.WorkspaceSymbolParams{ 1041 Query: query, 1042 } 1043 gotSymbols, err := r.server.Symbol(r.ctx, params) 1044 if err != nil { 1045 t.Fatal(err) 1046 } 1047 got, err := tests.WorkspaceSymbolsString(r.ctx, r.data, uri, gotSymbols) 1048 if err != nil { 1049 t.Fatal(err) 1050 } 1051 got = filepath.ToSlash(tests.Normalize(got, r.normalizers)) 1052 want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { 1053 return []byte(got), nil 1054 })) 1055 if diff := tests.Diff(want, got); diff != "" { 1056 t.Error(diff) 1057 } 1058 } 1059 1060 func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) { 1061 m, err := r.data.Mapper(spn.URI()) 1062 if err != nil { 1063 t.Fatal(err) 1064 } 1065 loc, err := m.Location(spn) 1066 if err != nil { 1067 t.Fatalf("failed for %v: %v", loc, err) 1068 } 1069 tdpp := protocol.TextDocumentPositionParams{ 1070 TextDocument: protocol.TextDocumentIdentifier{ 1071 URI: protocol.URIFromSpanURI(spn.URI()), 1072 }, 1073 Position: loc.Range.Start, 1074 } 1075 params := &protocol.SignatureHelpParams{ 1076 TextDocumentPositionParams: tdpp, 1077 } 1078 got, err := r.server.SignatureHelp(r.ctx, params) 1079 if err != nil { 1080 // Only fail if we got an error we did not expect. 1081 if want != nil { 1082 t.Fatal(err) 1083 } 1084 return 1085 } 1086 if want == nil { 1087 if got != nil { 1088 t.Errorf("expected no signature, got %v", got) 1089 } 1090 return 1091 } 1092 if got == nil { 1093 t.Fatalf("expected %v, got nil", want) 1094 } 1095 if diff := tests.DiffSignatures(spn, want, got); diff != "" { 1096 t.Error(diff) 1097 } 1098 } 1099 1100 func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { 1101 m, err := r.data.Mapper(uri) 1102 if err != nil { 1103 t.Fatal(err) 1104 } 1105 got, err := r.server.DocumentLink(r.ctx, &protocol.DocumentLinkParams{ 1106 TextDocument: protocol.TextDocumentIdentifier{ 1107 URI: protocol.URIFromSpanURI(uri), 1108 }, 1109 }) 1110 if err != nil { 1111 t.Fatal(err) 1112 } 1113 if diff := tests.DiffLinks(m, wantLinks, got); diff != "" { 1114 t.Error(diff) 1115 } 1116 } 1117 1118 func TestBytesOffset(t *testing.T) { 1119 tests := []struct { 1120 text string 1121 pos protocol.Position 1122 want int 1123 }{ 1124 {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, 1125 {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, 1126 {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, 1127 {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5}, 1128 {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6}, 1129 {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1}, 1130 {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, 1131 {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: 3}, 1132 {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, 1133 {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, 1134 {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: 7}, 1135 {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, 1136 {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1}, 1137 {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, 1138 } 1139 1140 for i, test := range tests { 1141 fname := fmt.Sprintf("test %d", i) 1142 fset := token.NewFileSet() 1143 f := fset.AddFile(fname, -1, len(test.text)) 1144 f.SetLinesForContent([]byte(test.text)) 1145 uri := span.URIFromPath(fname) 1146 converter := span.NewContentConverter(fname, []byte(test.text)) 1147 mapper := &protocol.ColumnMapper{ 1148 URI: uri, 1149 Converter: converter, 1150 Content: []byte(test.text), 1151 } 1152 got, err := mapper.Point(test.pos) 1153 if err != nil && test.want != -1 { 1154 t.Errorf("unexpected error: %v", err) 1155 } 1156 if err == nil && got.Offset() != test.want { 1157 t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got.Offset()) 1158 } 1159 } 1160 } 1161 1162 func (r *runner) collectDiagnostics(view source.View) { 1163 if r.diagnostics != nil { 1164 return 1165 } 1166 r.diagnostics = make(map[span.URI][]*source.Diagnostic) 1167 1168 snapshot, release := view.Snapshot(r.ctx) 1169 defer release() 1170 1171 // Always run diagnostics with analysis. 1172 r.server.diagnose(r.ctx, snapshot, true) 1173 for uri, reports := range r.server.diagnostics { 1174 var diagnostics []*source.Diagnostic 1175 for _, report := range reports.reports { 1176 for _, d := range report.diags { 1177 diagnostics = append(diagnostics, &source.Diagnostic{ 1178 Range: d.Range, 1179 Message: d.Message, 1180 Related: d.Related, 1181 Severity: d.Severity, 1182 Source: d.Source, 1183 Tags: d.Tags, 1184 }) 1185 } 1186 r.diagnostics[uri] = diagnostics 1187 } 1188 } 1189 }