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