1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: jsdoc @link ", () => { 3 const config: File = { 4 path: "/a/tsconfig.json", 5 content: `{ 6"compilerOptions": { 7"checkJs": true, 8"noEmit": true 9} 10"files": ["someFile1.js"] 11} 12` 13 }; 14 function assertQuickInfoJSDoc(file: File, options: { 15 displayPartsForJSDoc: boolean, 16 command: protocol.CommandTypes, 17 tags: string | unknown[] | undefined, 18 documentation: string | unknown[] 19 }) { 20 21 const { command, displayPartsForJSDoc, tags, documentation } = options; 22 const session = createSession(createServerHost([file, config])); 23 session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); 24 openFilesForSession([file], session); 25 const indexOfX = file.content.indexOf("x"); 26 const quickInfo = session.executeCommandSeq<protocol.QuickInfoRequest>({ 27 command: command as protocol.CommandTypes.Quickinfo, 28 arguments: { 29 file: file.path, 30 position: indexOfX, 31 } as protocol.FileLocationRequestArgs 32 }).response; 33 const summaryAndLocation = command === protocol.CommandTypes.Quickinfo ? { 34 displayString: "var x: number", 35 start: { 36 line: 3, 37 offset: 5, 38 }, 39 end: { 40 line: 3, 41 offset: 6, 42 } 43 } : { 44 displayParts: [{ 45 kind: "keyword", 46 text: "var", 47 }, { 48 kind: "space", 49 text: " ", 50 }, { 51 kind: "localName", 52 text: "x", 53 }, { 54 kind: "punctuation", 55 text: ":", 56 }, { 57 kind: "space", 58 text: " ", 59 }, { 60 kind: "keyword", 61 text: "number", 62 }], 63 textSpan: { 64 length: 1, 65 start: 38, 66 } 67 }; 68 assert.deepEqual(quickInfo, { 69 kind: "var", 70 kindModifiers: "", 71 ...summaryAndLocation, 72 documentation, 73 tags 74 }); 75 } 76 77 const linkInTag: File = { 78 path: "/a/someFile1.js", 79 content: `class C { } 80/** @wat {@link C} */ 81var x = 1` 82 }; 83 const linkInComment: File = { 84 path: "/a/someFile1.js", 85 content: `class C { } 86 /** {@link C} */ 87var x = 1 88;` 89 }; 90 91 it("for quickinfo, should provide display parts plus a span for a working link in a tag", () => { 92 assertQuickInfoJSDoc(linkInTag, { 93 command: protocol.CommandTypes.Quickinfo, 94 displayPartsForJSDoc: true, 95 documentation: [], 96 tags: [{ 97 name: "wat", 98 text: [{ 99 kind: "text", 100 text: "", 101 }, { 102 kind: "link", 103 text: "{@link ", 104 }, { 105 kind: "linkName", 106 target: { 107 end: { 108 line: 1, 109 offset: 12, 110 }, 111 file: "/a/someFile1.js", 112 start: { 113 line: 1, 114 offset: 1, 115 }, 116 }, 117 text: "C", 118 }, { 119 kind: "link", 120 text: "}", 121 }] 122 }], 123 }); 124 }); 125 it("for quickinfo, should provide a string for a working link in a tag", () => { 126 assertQuickInfoJSDoc(linkInTag, { 127 command: protocol.CommandTypes.Quickinfo, 128 displayPartsForJSDoc: false, 129 documentation: "", 130 tags: [{ 131 name: "wat", 132 text: "{@link C}" 133 }], 134 }); 135 }); 136 it("for quickinfo, should provide display parts for a working link in a comment", () => { 137 assertQuickInfoJSDoc(linkInComment, { 138 command: protocol.CommandTypes.Quickinfo, 139 displayPartsForJSDoc: true, 140 documentation: [{ 141 kind: "text", 142 text: "", 143 }, { 144 kind: "link", 145 text: "{@link ", 146 }, { 147 kind: "linkName", 148 target: { 149 end: { 150 line: 1, 151 offset: 12, 152 }, 153 file: "/a/someFile1.js", 154 start: { 155 line: 1, 156 offset: 1, 157 }, 158 }, 159 text: "C", 160 }, { 161 kind: "link", 162 text: "}", 163 }], 164 tags: [], 165 }); 166 }); 167 it("for quickinfo, should provide a string for a working link in a comment", () => { 168 assertQuickInfoJSDoc(linkInComment, { 169 command: protocol.CommandTypes.Quickinfo, 170 displayPartsForJSDoc: false, 171 documentation: "{@link C}", 172 tags: [], 173 }); 174 }); 175 176 it("for quickinfo-full, should provide display parts plus a span for a working link in a tag", () => { 177 assertQuickInfoJSDoc(linkInTag, { 178 command: protocol.CommandTypes.QuickinfoFull, 179 displayPartsForJSDoc: true, 180 documentation: [], 181 tags: [{ 182 name: "wat", 183 text: [{ 184 kind: "text", 185 text: "", 186 }, { 187 kind: "link", 188 text: "{@link ", 189 }, { 190 kind: "linkName", 191 target: { 192 fileName: "/a/someFile1.js", 193 textSpan: { 194 length: 11, 195 start: 0 196 }, 197 }, 198 text: "C", 199 }, { 200 kind: "link", 201 text: "}", 202 }] 203 }], 204 }); 205 }); 206 it("for quickinfo-full, should provide a string for a working link in a tag", () => { 207 assertQuickInfoJSDoc(linkInTag, { 208 command: protocol.CommandTypes.QuickinfoFull, 209 displayPartsForJSDoc: false, 210 documentation: [], 211 tags: [{ 212 name: "wat", 213 text: "{@link C}" 214 }], 215 }); 216 }); 217 it("for quickinfo-full, should provide display parts plus a span for a working link in a comment", () => { 218 assertQuickInfoJSDoc(linkInComment, { 219 command: protocol.CommandTypes.QuickinfoFull, 220 displayPartsForJSDoc: true, 221 documentation: [{ 222 kind: "text", 223 text: "", 224 }, { 225 kind: "link", 226 text: "{@link ", 227 }, { 228 kind: "linkName", 229 target: { 230 fileName: "/a/someFile1.js", 231 textSpan: { 232 length: 11, 233 start: 0 234 }, 235 }, 236 text: "C", 237 }, { 238 kind: "link", 239 text: "}", 240 }], 241 tags: undefined, 242 }); 243 }); 244 it("for quickinfo-full, should provide a string for a working link in a comment", () => { 245 assertQuickInfoJSDoc(linkInComment, { 246 command: protocol.CommandTypes.QuickinfoFull, 247 displayPartsForJSDoc: false, 248 documentation: [{ 249 kind: "text", 250 text: "", 251 }, { 252 kind: "link", 253 text: "{@link ", 254 }, { 255 kind: "linkName", 256 target: { 257 fileName: "/a/someFile1.js", 258 textSpan: { 259 length: 11, 260 start: 0 261 }, 262 }, 263 text: "C", 264 }, { 265 kind: "link", 266 text: "}", 267 }], 268 tags: [], 269 }); 270 }); 271 272 function assertSignatureHelpJSDoc(options: { 273 displayPartsForJSDoc: boolean, 274 command: protocol.CommandTypes, 275 documentation: string | unknown[], 276 tags: unknown[] 277 }) { 278 const linkInParamTag: File = { 279 path: "/a/someFile1.js", 280 content: `class C { } 281/** @param y - {@link C} */ 282function x(y) { } 283x(1)` 284 }; 285 286 const { command, displayPartsForJSDoc, documentation, tags } = options; 287 const session = createSession(createServerHost([linkInParamTag, config])); 288 session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); 289 openFilesForSession([linkInParamTag], session); 290 const indexOfX = linkInParamTag.content.lastIndexOf("1"); 291 const signatureHelp = session.executeCommandSeq<protocol.SignatureHelpRequest>({ 292 command: command as protocol.CommandTypes.SignatureHelp, 293 arguments: { 294 triggerReason: { 295 kind: "invoked" 296 }, 297 file: linkInParamTag.path, 298 position: indexOfX, 299 } as protocol.SignatureHelpRequestArgs 300 }).response; 301 const applicableSpan = command === protocol.CommandTypes.SignatureHelp ? { 302 end: { 303 line: 4, 304 offset: 4 305 }, 306 start: { 307 line: 4, 308 offset: 3 309 } 310 } : { 311 length: 1, 312 start: 60 313 }; 314 assert.deepEqual(signatureHelp, { 315 applicableSpan, 316 argumentCount: 1, 317 argumentIndex: 0, 318 selectedItemIndex: 0, 319 items: [{ 320 documentation: [], 321 isVariadic: false, 322 parameters: [{ 323 displayParts: [{ 324 kind: "parameterName", 325 text: "y" 326 }, { 327 kind: "punctuation", 328 text: ":" 329 }, { 330 kind: "space", 331 text: " " 332 }, { 333 kind: "keyword", 334 text: "any" 335 }], 336 documentation, 337 isOptional: false, 338 isRest: false, 339 name: "y" 340 }], 341 prefixDisplayParts: [ 342 { 343 kind: "functionName", 344 text: "x", 345 }, 346 { 347 kind: "punctuation", 348 text: "(", 349 }, 350 ], 351 separatorDisplayParts: [ 352 { 353 kind: "punctuation", 354 text: ",", 355 }, 356 { 357 kind: "space", 358 text: " ", 359 }, 360 ], 361 suffixDisplayParts: [ 362 { 363 kind: "punctuation", 364 text: ")", 365 }, 366 { 367 kind: "punctuation", 368 text: ":", 369 }, 370 { 371 kind: "space", 372 text: " ", 373 }, 374 { 375 kind: "keyword", 376 text: "void", 377 } 378 ], 379 tags, 380 }], 381 }); 382 } 383 it("for signature help, should provide a string for a working link in a comment", () => { 384 assertSignatureHelpJSDoc({ 385 command: protocol.CommandTypes.SignatureHelp, 386 displayPartsForJSDoc: false, 387 tags: [{ 388 name: "param", 389 text: "y - {@link C}" 390 }], 391 documentation: [{ 392 kind: "text", 393 text: "- " 394 }, { 395 kind: "link", 396 text: "{@link " 397 }, { 398 kind: "linkName", 399 target: { 400 file: "/a/someFile1.js", 401 start: { 402 line: 1, 403 offset: 1 404 }, 405 end: { 406 line: 1, 407 offset: 12 408 } 409 }, 410 text: "C" 411 }, { 412 kind: "link", 413 text: "}" 414 }], 415 }); 416 }); 417 it("for signature help, should provide display parts for a working link in a comment", () => { 418 const tags = [{ 419 name: "param", 420 text: [{ 421 kind: "parameterName", 422 text: "y" 423 }, { 424 kind: "space", 425 text: " " 426 }, { 427 kind: "text", 428 text: "- " 429 }, { 430 kind: "link", 431 text: "{@link " 432 }, { 433 kind: "linkName", 434 target: { 435 file: "/a/someFile1.js", 436 start: { 437 line: 1, 438 offset: 1 439 }, 440 end: { 441 line: 1, 442 offset: 12 443 } 444 }, 445 text: "C" 446 }, { 447 kind: "link", 448 text: "}" 449 }] 450 }]; 451 assertSignatureHelpJSDoc({ 452 command: protocol.CommandTypes.SignatureHelp, 453 displayPartsForJSDoc: true, 454 tags, 455 documentation: tags[0].text.slice(2) 456 }); 457 }); 458 it("for signature help-full, should provide a string for a working link in a comment", () => { 459 assertSignatureHelpJSDoc({ 460 command: protocol.CommandTypes.SignatureHelpFull, 461 displayPartsForJSDoc: false, 462 tags: [{ 463 name: "param", 464 text: "y - {@link C}" 465 }], 466 documentation: [{ 467 kind: "text", 468 text: "- " 469 }, { 470 kind: "link", 471 text: "{@link " 472 }, { 473 kind: "linkName", 474 target: { 475 fileName: "/a/someFile1.js", 476 textSpan: { 477 length: 11, 478 start: 0 479 } 480 }, 481 text: "C" 482 }, { 483 kind: "link", 484 text: "}" 485 }], 486 }); 487 }); 488 it("for signature help-full, should provide display parts for a working link in a comment", () => { 489 const tags = [{ 490 name: "param", 491 text: [{ 492 kind: "parameterName", 493 text: "y" 494 }, { 495 kind: "space", 496 text: " " 497 }, { 498 kind: "text", 499 text: "- " 500 }, { 501 kind: "link", 502 text: "{@link " 503 }, { 504 kind: "linkName", 505 target: { 506 fileName: "/a/someFile1.js", 507 textSpan: { 508 length: 11, 509 start: 0 510 } 511 }, 512 text: "C" 513 }, { 514 kind: "link", 515 text: "}" 516 }] 517 }]; 518 assertSignatureHelpJSDoc({ 519 command: protocol.CommandTypes.SignatureHelpFull, 520 displayPartsForJSDoc: true, 521 tags, 522 documentation: tags[0].text.slice(2), 523 }); 524 }); 525 526 function assertCompletionsJSDoc(options: { 527 displayPartsForJSDoc: boolean, 528 command: protocol.CommandTypes, 529 tags: unknown[] 530 }) { 531 const linkInParamJSDoc: File = { 532 path: "/a/someFile1.js", 533 content: `class C { } 534/** @param x - see {@link C} */ 535function foo (x) { } 536foo` 537 }; 538 const { command, displayPartsForJSDoc, tags } = options; 539 const session = createSession(createServerHost([linkInParamJSDoc, config])); 540 session.getProjectService().setHostConfiguration({ preferences: { displayPartsForJSDoc } }); 541 openFilesForSession([linkInParamJSDoc], session); 542 const indexOfFoo = linkInParamJSDoc.content.lastIndexOf("fo"); 543 const completions = session.executeCommandSeq<protocol.CompletionDetailsRequest>({ 544 command: command as protocol.CommandTypes.CompletionDetails, 545 arguments: { 546 entryNames: ["foo"], 547 file: linkInParamJSDoc.path, 548 position: indexOfFoo, 549 } as protocol.CompletionDetailsRequestArgs 550 }).response; 551 assert.deepEqual(completions, [{ 552 codeActions: undefined, 553 displayParts: [{ 554 kind: "keyword", 555 text: "function", 556 }, { 557 kind: "space", 558 text: " ", 559 }, { 560 kind: "functionName", 561 text: "foo", 562 }, { 563 kind: "punctuation", 564 text: "(", 565 }, { 566 kind: "parameterName", 567 text: "x", 568 }, { 569 kind: "punctuation", 570 text: ":", 571 }, { 572 kind: "space", 573 text: " ", 574 }, { 575 kind: "keyword", 576 text: "any", 577 }, { 578 kind: "punctuation", 579 text: ")", 580 }, { 581 kind: "punctuation", 582 text: ":", 583 }, { 584 kind: "space", 585 text: " ", 586 }, { 587 kind: "keyword", 588 text: "void", 589 }], 590 documentation: [], 591 kind: "function", 592 kindModifiers: "", 593 name: "foo", 594 source: undefined, 595 sourceDisplay: undefined, 596 tags, 597 }]); 598 } 599 it("for completions, should provide display parts for a working link in a comment", () => { 600 assertCompletionsJSDoc({ 601 command: protocol.CommandTypes.CompletionDetails, 602 displayPartsForJSDoc: true, 603 tags: [{ 604 name: "param", 605 text: [{ 606 kind: "parameterName", 607 text: "x" 608 }, { 609 kind: "space", 610 text: " " 611 }, { 612 kind: "text", 613 text: "- see " 614 }, { 615 kind: "link", 616 text: "{@link " 617 }, { 618 kind: "linkName", 619 target: { 620 file: "/a/someFile1.js", 621 end: { 622 line: 1, 623 offset: 12, 624 }, 625 start: { 626 line: 1, 627 offset: 1, 628 } 629 }, 630 text: "C" 631 }, { 632 kind: "link", 633 text: "}" 634 }], 635 }], 636 }); 637 }); 638 it("for completions, should provide a string for a working link in a comment", () => { 639 assertCompletionsJSDoc({ 640 command: protocol.CommandTypes.CompletionDetails, 641 displayPartsForJSDoc: false, 642 tags: [{ 643 name: "param", 644 text: "x - see {@link C}", 645 }], 646 }); 647 }); 648 it("for completions-full, should provide display parts for a working link in a comment", () => { 649 assertCompletionsJSDoc({ 650 command: protocol.CommandTypes.CompletionDetailsFull, 651 displayPartsForJSDoc: true, 652 tags: [{ 653 name: "param", 654 text: [{ 655 kind: "parameterName", 656 text: "x" 657 }, { 658 kind: "space", 659 text: " " 660 }, { 661 kind: "text", 662 text: "- see " 663 }, { 664 kind: "link", 665 text: "{@link " 666 }, { 667 kind: "linkName", 668 target: { 669 fileName: "/a/someFile1.js", 670 textSpan: { 671 length: 11, 672 start: 0 673 } 674 }, 675 text: "C" 676 }, { 677 kind: "link", 678 text: "}" 679 }], 680 }], 681 }); 682 }); 683 it("for completions-full, should provide a string for a working link in a comment", () => { 684 assertCompletionsJSDoc({ 685 command: protocol.CommandTypes.CompletionDetailsFull, 686 displayPartsForJSDoc: false, 687 tags: [{ 688 name: "param", 689 text: "x - see {@link C}", 690 }], 691 }); 692 }); 693 }); 694 695 describe("unittests:: tsserver:: jsDoc tag check", () => { 696 it("works jsDoc tag check", () => { 697 const aUser: File = { 698 path: "/a.ts", 699 content: `import { y } from "./b"; 700 y.test() 701 y.test2() 702` 703 }; 704 const bUser: File = { 705 path: "/b.ts", 706 content: ` 707export class y { 708/** 709 * @ignore 710 */ 711static test(): void { 712 713} 714/** 715 * @systemApi 716 */ 717static test2(): void { 718 719} 720}` 721 }; 722 const tsconfigFile: File = { 723 path: "/tsconfig.json", 724 content: JSON.stringify({ 725 compilerOptions: { 726 target: "es6", 727 module: "es6", 728 baseUrl: "./", // all paths are relative to the baseUrl 729 paths: { 730 "~/*": ["*"] // resolve any `~/foo/bar` to `<baseUrl>/foo/bar` 731 } 732 }, 733 exclude: [ 734 "api", 735 "build", 736 "node_modules", 737 "public", 738 "seeds", 739 "sql_updates", 740 "tests.build" 741 ] 742 }) 743 }; 744 745 const projectFiles = [aUser, bUser, tsconfigFile]; 746 const host = createServerHost(projectFiles); 747 host.getFileCheckedModuleInfo = (sourceFilePath: string) => { 748 Debug.log(sourceFilePath); 749 return { 750 fileNeedCheck: true, 751 checkPayload: undefined, 752 currentFileName: "", 753 } 754 } 755 host.getJsDocNodeCheckedConfig = (jsDocFileCheckInfo: FileCheckModuleInfo, sourceFilePath: string) => { 756 Debug.log(jsDocFileCheckInfo.fileNeedCheck.toString()); 757 Debug.log(sourceFilePath); 758 return { 759 nodeNeedCheck: true, 760 checkConfig: [{ 761 tagName: ["ignore"], 762 message: "This API has been ignored. exercise caution when using this API.", 763 needConditionCheck: false, 764 type: DiagnosticCategory.Warning, 765 specifyCheckConditionFuncName: "", 766 tagNameShouldExisted: false, 767 },{ 768 tagName: ["systemApi"], 769 message: "This API is used to develop system apps. exercise caution when using this API.", 770 needConditionCheck: false, 771 type: DiagnosticCategory.Warning, 772 specifyCheckConditionFuncName: "", 773 tagNameShouldExisted: false, 774 }] 775 }; 776 }; 777 host.getJsDocNodeConditionCheckedResult = (jsDocFileCheckInfo: FileCheckModuleInfo, jsDocs: JSDocTagInfo[]) => { 778 Debug.log(jsDocFileCheckInfo.fileNeedCheck.toString()); 779 Debug.log(jsDocs.toString()); 780 return { 781 valid: false, 782 message: "", 783 type: DiagnosticCategory.Warning 784 }; 785 }; 786 const session = createSession(host); 787 const projectService = session.getProjectService(); 788 const { configFileName } = projectService.openClientFile(aUser.path); 789 790 assert.isDefined(configFileName, `should find config`); 791 792 const project = projectService.configuredProjects.get(tsconfigFile.path)!; 793 const response = project.getLanguageService().getSuggestionDiagnostics(aUser.path); 794 assert.deepEqual(response[0].messageText, "This API has been ignored. exercise caution when using this API."); 795 assert.deepEqual(response[1].messageText, "This API is used to develop system apps. exercise caution when using this API."); 796 }); 797 }); 798} 799