1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: Project Errors", () => { 3 function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: readonly string[]): void { 4 assert.isTrue(projectFiles !== undefined, "missing project files"); 5 checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); 6 } 7 8 function checkProjectErrorsWorker(errors: readonly Diagnostic[], expectedErrors: readonly string[]): void { 9 assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); 10 if (expectedErrors.length) { 11 for (let i = 0; i < errors.length; i++) { 12 const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n"); 13 const expectedMessage = expectedErrors[i]; 14 assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); 15 } 16 } 17 } 18 19 function checkDiagnosticsWithLinePos(errors: server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { 20 assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); 21 if (expectedErrors.length) { 22 zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => { 23 assert.isTrue(startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); 24 }); 25 } 26 } 27 28 it("external project - diagnostics for missing files", () => { 29 const file1 = { 30 path: "/a/b/app.ts", 31 content: "" 32 }; 33 const file2 = { 34 path: "/a/b/applib.ts", 35 content: "" 36 }; 37 const host = createServerHost([file1, libFile]); 38 const session = createSession(host); 39 const projectService = session.getProjectService(); 40 const projectFileName = "/a/b/test.csproj"; 41 const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { 42 type: "request", 43 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 44 seq: 2, 45 arguments: { projectFileName } 46 }; 47 48 { 49 projectService.openExternalProject({ 50 projectFileName, 51 options: {}, 52 rootFiles: toExternalFiles([file1.path, file2.path]) 53 }); 54 55 checkNumberOfProjects(projectService, { externalProjects: 1 }); 56 const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; 57 // only file1 exists - expect error 58 checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); 59 } 60 host.renameFile(file1.path, file2.path); 61 { 62 // only file2 exists - expect error 63 checkNumberOfProjects(projectService, { externalProjects: 1 }); 64 const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; 65 checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); 66 } 67 68 host.writeFile(file1.path, file1.content); 69 { 70 // both files exist - expect no errors 71 checkNumberOfProjects(projectService, { externalProjects: 1 }); 72 const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; 73 checkDiagnosticsWithLinePos(diags, []); 74 } 75 }); 76 77 it("configured projects - diagnostics for missing files", () => { 78 const file1 = { 79 path: "/a/b/app.ts", 80 content: "" 81 }; 82 const file2 = { 83 path: "/a/b/applib.ts", 84 content: "" 85 }; 86 const config = { 87 path: "/a/b/tsconfig.json", 88 content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) 89 }; 90 const host = createServerHost([file1, config, libFile]); 91 const session = createSession(host); 92 const projectService = session.getProjectService(); 93 openFilesForSession([file1], session); 94 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 95 const project = configuredProjectAt(projectService, 0); 96 const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { 97 type: "request", 98 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 99 seq: 2, 100 arguments: { projectFileName: project.getProjectName() } 101 }; 102 let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; 103 checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); 104 105 host.writeFile(file2.path, file2.content); 106 107 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 108 diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; 109 checkDiagnosticsWithLinePos(diags, []); 110 }); 111 112 it("configured projects - diagnostics for corrupted config 1", () => { 113 const file1 = { 114 path: "/a/b/app.ts", 115 content: "" 116 }; 117 const file2 = { 118 path: "/a/b/lib.ts", 119 content: "" 120 }; 121 const correctConfig = { 122 path: "/a/b/tsconfig.json", 123 content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) 124 }; 125 const corruptedConfig = { 126 path: correctConfig.path, 127 content: correctConfig.content.substr(1) 128 }; 129 const host = createServerHost([file1, file2, corruptedConfig]); 130 const projectService = createProjectService(host); 131 132 projectService.openClientFile(file1.path); 133 { 134 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 135 const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; 136 assert.isTrue(configuredProject !== undefined, "should find configured project"); 137 checkProjectErrors(configuredProject, []); 138 const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); 139 checkProjectErrorsWorker(projectErrors, [ 140 "'{' expected." 141 ]); 142 assert.isNotNull(projectErrors[0].file); 143 assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); 144 } 145 // fix config and trigger watcher 146 host.writeFile(correctConfig.path, correctConfig.content); 147 { 148 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 149 const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; 150 assert.isTrue(configuredProject !== undefined, "should find configured project"); 151 checkProjectErrors(configuredProject, []); 152 const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); 153 checkProjectErrorsWorker(projectErrors, []); 154 } 155 }); 156 157 it("configured projects - diagnostics for corrupted config 2", () => { 158 const file1 = { 159 path: "/a/b/app.ts", 160 content: "" 161 }; 162 const file2 = { 163 path: "/a/b/lib.ts", 164 content: "" 165 }; 166 const correctConfig = { 167 path: "/a/b/tsconfig.json", 168 content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) 169 }; 170 const corruptedConfig = { 171 path: correctConfig.path, 172 content: correctConfig.content.substr(1) 173 }; 174 const host = createServerHost([file1, file2, correctConfig]); 175 const projectService = createProjectService(host); 176 177 projectService.openClientFile(file1.path); 178 { 179 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 180 const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; 181 assert.isTrue(configuredProject !== undefined, "should find configured project"); 182 checkProjectErrors(configuredProject, []); 183 const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); 184 checkProjectErrorsWorker(projectErrors, []); 185 } 186 // break config and trigger watcher 187 host.writeFile(corruptedConfig.path, corruptedConfig.content); 188 { 189 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 190 const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; 191 assert.isTrue(configuredProject !== undefined, "should find configured project"); 192 checkProjectErrors(configuredProject, []); 193 const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); 194 checkProjectErrorsWorker(projectErrors, [ 195 "'{' expected." 196 ]); 197 assert.isNotNull(projectErrors[0].file); 198 assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); 199 } 200 }); 201 }); 202 203 describe("unittests:: tsserver:: Project Errors are reported as appropriate", () => { 204 it("document is not contained in project", () => { 205 const file1 = { 206 path: "/a/b/app.ts", 207 content: "" 208 }; 209 const corruptedConfig = { 210 path: "/a/b/tsconfig.json", 211 content: "{" 212 }; 213 const host = createServerHost([file1, corruptedConfig]); 214 const projectService = createProjectService(host); 215 216 projectService.openClientFile(file1.path); 217 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 218 219 const project = projectService.findProject(corruptedConfig.path)!; 220 checkProjectRootFiles(project, [file1.path]); 221 }); 222 223 describe("when opening new file that doesnt exist on disk yet", () => { 224 function verifyNonExistentFile(useProjectRoot: boolean) { 225 const folderPath = "/user/someuser/projects/someFolder"; 226 const fileInRoot: File = { 227 path: `/src/somefile.d.ts`, 228 content: "class c { }" 229 }; 230 const fileInProjectRoot: File = { 231 path: `${folderPath}/src/somefile.d.ts`, 232 content: "class c { }" 233 }; 234 const host = createServerHost([libFile, fileInRoot, fileInProjectRoot]); 235 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host), useInferredProjectPerProjectRoot: true }); 236 237 const untitledFile = "untitled:Untitled-1"; 238 const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; 239 const refPathNotFound2 = "./src/somefile.d.ts"; 240 const fileContent = `/// <reference path="${refPathNotFound1}" /> 241/// <reference path="${refPathNotFound2}" />`; 242 session.executeCommandSeq<protocol.OpenRequest>({ 243 command: server.CommandNames.Open, 244 arguments: { 245 file: untitledFile, 246 fileContent, 247 scriptKindName: "TS", 248 projectRootPath: useProjectRoot ? folderPath : undefined 249 } 250 }); 251 appendAllScriptInfos(session.getProjectService(), session.logger.logs); 252 253 // Since this is not js project so no typings are queued 254 host.checkTimeoutQueueLength(0); 255 verifyGetErrRequest({ session, host, files: [untitledFile] }); 256 baselineTsserverLogs("projectErrors", `when opening new file that doesnt exist on disk yet ${useProjectRoot ? "with projectRoot" : "without projectRoot"}`, session); 257 } 258 259 it("has projectRoot", () => { 260 verifyNonExistentFile(/*useProjectRoot*/ true); 261 }); 262 263 it("does not have projectRoot", () => { 264 verifyNonExistentFile(/*useProjectRoot*/ false); 265 }); 266 }); 267 268 it("folder rename updates project structure and reports no errors", () => { 269 const projectDir = "/a/b/projects/myproject"; 270 const app: File = { 271 path: `${projectDir}/bar/app.ts`, 272 content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" 273 }; 274 const foo: File = { 275 path: `${projectDir}/foo/foo.ts`, 276 content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" 277 }; 278 const configFile: File = { 279 path: `${projectDir}/tsconfig.json`, 280 content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) 281 }; 282 const host = createServerHost([app, foo, configFile]); 283 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 284 285 session.executeCommandSeq<protocol.OpenRequest>({ 286 command: server.CommandNames.Open, 287 arguments: { file: app.path, } 288 }); 289 verifyGetErrRequest({ session, host, files: [app] }); 290 291 host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); 292 host.runQueuedTimeoutCallbacks(); 293 host.runQueuedTimeoutCallbacks(); 294 verifyGetErrRequest({ session, host, files: [app] }); 295 baselineTsserverLogs("projectErrors", `folder rename updates project structure and reports no errors`, session); 296 }); 297 298 it("Getting errors before opening file", () => { 299 const file: File = { 300 path: "/a/b/project/file.ts", 301 content: "let x: number = false;" 302 }; 303 const host = createServerHost([file, libFile]); 304 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 305 session.executeCommandSeq<protocol.GeterrRequest>({ 306 command: server.CommandNames.Geterr, 307 arguments: { 308 delay: 0, 309 files: [file.path] 310 } 311 }); 312 313 host.checkTimeoutQueueLengthAndRun(1); 314 baselineTsserverLogs("projectErrors", "getting errors before opening file", session); 315 }); 316 317 it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { 318 const app: File = { 319 path: `${tscWatch.projectRoot}/src/client/app.js`, 320 content: "" 321 }; 322 const serverUtilities: File = { 323 path: `${tscWatch.projectRoot}/src/server/utilities.js`, 324 content: `function getHostName() { return "hello"; } export { getHostName };` 325 }; 326 const backendTest: File = { 327 path: `${tscWatch.projectRoot}/test/backend/index.js`, 328 content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` 329 }; 330 const files = [libFile, app, serverUtilities, backendTest]; 331 const host = createServerHost(files); 332 const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 333 openFilesForSession([{ file: app, projectRootPath: tscWatch.projectRoot }], session); 334 openFilesForSession([{ file: backendTest, projectRootPath: tscWatch.projectRoot }], session); 335 verifyGetErrRequest({ session, host, files: [backendTest.path, app.path] }); 336 closeFilesForSession([backendTest], session); 337 openFilesForSession([{ file: serverUtilities.path, projectRootPath: tscWatch.projectRoot }], session); 338 verifyGetErrRequest({ session, host, files: [serverUtilities.path, app.path] }); 339 baselineTsserverLogs("projectErrors", `reports errors correctly when file referenced by inferred project root, is opened right after closing the root file`, session); 340 }); 341 342 it("Correct errors when resolution resolves to file that has same ambient module and is also module", () => { 343 const projectRootPath = "/users/username/projects/myproject"; 344 const aFile: File = { 345 path: `${projectRootPath}/src/a.ts`, 346 content: `import * as myModule from "@custom/plugin"; 347function foo() { 348 // hello 349}` 350 }; 351 const config: File = { 352 path: `${projectRootPath}/tsconfig.json`, 353 content: JSON.stringify({ include: ["src"] }) 354 }; 355 const plugin: File = { 356 path: `${projectRootPath}/node_modules/@custom/plugin/index.d.ts`, 357 content: `import './proposed'; 358declare module '@custom/plugin' { 359 export const version: string; 360}` 361 }; 362 const pluginProposed: File = { 363 path: `${projectRootPath}/node_modules/@custom/plugin/proposed.d.ts`, 364 content: `declare module '@custom/plugin' { 365 export const bar = 10; 366}` 367 }; 368 const files = [libFile, aFile, config, plugin, pluginProposed]; 369 const host = createServerHost(files); 370 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 371 openFilesForSession([aFile], session); 372 373 checkErrors(); 374 375 session.executeCommandSeq<protocol.ChangeRequest>({ 376 command: protocol.CommandTypes.Change, 377 arguments: { 378 file: aFile.path, 379 line: 3, 380 offset: 8, 381 endLine: 3, 382 endOffset: 8, 383 insertString: "o" 384 } 385 }); 386 checkErrors(); 387 baselineTsserverLogs("projectErrors", `correct errors when resolution resolves to file that has same ambient module and is also module`, session); 388 389 function checkErrors() { 390 host.checkTimeoutQueueLength(0); 391 verifyGetErrRequest({ session, host, files: [aFile] }); 392 } 393 }); 394 395 describe("when semantic error returns includes global error", () => { 396 const file: File = { 397 path: `${tscWatch.projectRoot}/ui.ts`, 398 content: `const x = async (_action: string) => { 399};` 400 }; 401 const config: File = { 402 path: `${tscWatch.projectRoot}/tsconfig.json`, 403 content: "{}" 404 }; 405 verifyGetErrScenario({ 406 scenario: "projectErrors", 407 subScenario: "when semantic error returns includes global error", 408 allFiles: () => [libFile, file, config], 409 openFiles: () => [file], 410 getErrRequest: () => [file], 411 getErrForProjectRequest: () => [{ project: file, files: [file] }], 412 syncDiagnostics: () => [{ file, project: config }], 413 }); 414 }); 415 }); 416 417 describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { 418 it("are generated when the config file has errors", () => { 419 const file: File = { 420 path: "/a/b/app.ts", 421 content: "let x = 10" 422 }; 423 const configFile: File = { 424 path: "/a/b/tsconfig.json", 425 content: `{ 426 "compilerOptions": { 427 "foo": "bar", 428 "allowJS": true 429 } 430 }` 431 }; 432 const host = createServerHost([file, libFile, configFile]); 433 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 434 openFilesForSession([file], session); 435 baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file has errors", session); 436 }); 437 438 it("are generated when the config file doesn't have errors", () => { 439 const file: File = { 440 path: "/a/b/app.ts", 441 content: "let x = 10" 442 }; 443 const configFile: File = { 444 path: "/a/b/tsconfig.json", 445 content: `{ 446 "compilerOptions": {} 447 }` 448 }; 449 const host = createServerHost([file, libFile, configFile]); 450 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 451 openFilesForSession([file], session); 452 baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file doesnt have errors", session); 453 }); 454 455 it("are generated when the config file changes", () => { 456 const file: File = { 457 path: "/a/b/app.ts", 458 content: "let x = 10" 459 }; 460 const configFile = { 461 path: "/a/b/tsconfig.json", 462 content: `{ 463 "compilerOptions": {} 464 }` 465 }; 466 467 const host = createServerHost([file, libFile, configFile]); 468 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 469 openFilesForSession([file], session); 470 471 configFile.content = `{ 472 "compilerOptions": { 473 "haha": 123 474 } 475 }`; 476 host.writeFile(configFile.path, configFile.content); 477 host.runQueuedTimeoutCallbacks(); 478 479 configFile.content = `{ 480 "compilerOptions": {} 481 }`; 482 host.writeFile(configFile.path, configFile.content); 483 host.runQueuedTimeoutCallbacks(); 484 baselineTsserverLogs("projectErrors", "configFileDiagnostic events are generated when the config file changes", session); 485 }); 486 487 it("are not generated when the config file does not include file opened and config file has errors", () => { 488 const file: File = { 489 path: "/a/b/app.ts", 490 content: "let x = 10" 491 }; 492 const file2: File = { 493 path: "/a/b/test.ts", 494 content: "let x = 10" 495 }; 496 const file3: File = { 497 path: "/a/b/test2.ts", 498 content: "let xy = 10" 499 }; 500 const configFile: File = { 501 path: "/a/b/tsconfig.json", 502 content: `{ 503 "compilerOptions": { 504 "foo": "bar", 505 "allowJS": true 506 }, 507 "files": ["app.ts"] 508 }` 509 }; 510 const host = createServerHost([file, libFile, configFile]); 511 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 512 openFilesForSession([file2], session); 513 openFilesForSession([file], session); 514 // We generate only if project is created when opening file from the project 515 openFilesForSession([file3], session); 516 baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file does not include file opened and config file has errors", session); 517 }); 518 519 it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { 520 const file: File = { 521 path: "/a/b/app.ts", 522 content: "let x = 10" 523 }; 524 const configFile: File = { 525 path: "/a/b/tsconfig.json", 526 content: `{ 527 "compilerOptions": { 528 "foo": "bar", 529 "allowJS": true 530 } 531 }` 532 }; 533 const host = createServerHost([file, libFile, configFile]); 534 const session = createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 535 openFilesForSession([file], session); 536 baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file has errors but suppressDiagnosticEvents is true", session); 537 }); 538 539 it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { 540 const file: File = { 541 path: "/a/b/app.ts", 542 content: "let x = 10" 543 }; 544 const file2: File = { 545 path: "/a/b/test.ts", 546 content: "let x = 10" 547 }; 548 const file3: File = { 549 path: "/a/b/test2.ts", 550 content: "let xy = 10" 551 }; 552 const configFile: File = { 553 path: "/a/b/tsconfig.json", 554 content: `{ 555 "files": ["app.ts"] 556 }` 557 }; 558 559 const host = createServerHost([file, file2, file3, libFile, configFile]); 560 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 561 openFilesForSession([file2], session); 562 openFilesForSession([file], session); 563 // We generate only if project is created when opening file from the project 564 openFilesForSession([file3], session); 565 baselineTsserverLogs("projectErrors", "configFileDiagnostic events are not generated when the config file does not include file opened and doesnt contain any errors", session); 566 }); 567 568 it("contains the project reference errors", () => { 569 const file: File = { 570 path: "/a/b/app.ts", 571 content: "let x = 10" 572 }; 573 const noSuchTsconfig = "no-such-tsconfig.json"; 574 const configFile: File = { 575 path: "/a/b/tsconfig.json", 576 content: `{ 577 "files": ["app.ts"], 578 "references": [{"path":"./${noSuchTsconfig}"}] 579 }` 580 }; 581 582 const host = createServerHost([file, libFile, configFile]); 583 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 584 openFilesForSession([file], session); 585 baselineTsserverLogs("projectErrors", "configFileDiagnostic events contains the project reference errors", session); 586 }); 587 }); 588 589 describe("unittests:: tsserver:: Project Errors dont include overwrite emit error", () => { 590 it("for inferred project", () => { 591 const f1 = { 592 path: "/a/b/f1.js", 593 content: "function test1() { }" 594 }; 595 const host = createServerHost([f1, libFile]); 596 const session = createSession(host); 597 openFilesForSession([f1], session); 598 599 const projectService = session.getProjectService(); 600 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 601 const projectName = projectService.inferredProjects[0].getProjectName(); 602 603 const diags = session.executeCommand({ 604 type: "request", 605 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 606 seq: 2, 607 arguments: { projectFileName: projectName } 608 } as server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly protocol.DiagnosticWithLinePosition[]; 609 assert.isTrue(diags.length === 0); 610 611 session.executeCommand({ 612 type: "request", 613 command: server.CommandNames.CompilerOptionsForInferredProjects, 614 seq: 3, 615 arguments: { options: { module: ModuleKind.CommonJS } } 616 } as server.protocol.SetCompilerOptionsForInferredProjectsRequest); 617 const diagsAfterUpdate = session.executeCommand({ 618 type: "request", 619 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 620 seq: 4, 621 arguments: { projectFileName: projectName } 622 } as server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly protocol.DiagnosticWithLinePosition[]; 623 assert.isTrue(diagsAfterUpdate.length === 0); 624 }); 625 626 it("for external project", () => { 627 const f1 = { 628 path: "/a/b/f1.js", 629 content: "function test1() { }" 630 }; 631 const host = createServerHost([f1, libFile]); 632 const session = createSession(host); 633 const projectService = session.getProjectService(); 634 const projectFileName = "/a/b/project.csproj"; 635 const externalFiles = toExternalFiles([f1.path]); 636 projectService.openExternalProject({ 637 projectFileName, 638 rootFiles: externalFiles, 639 options: {} 640 } as protocol.ExternalProject); 641 642 checkNumberOfProjects(projectService, { externalProjects: 1 }); 643 644 const diags = session.executeCommand({ 645 type: "request", 646 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 647 seq: 2, 648 arguments: { projectFileName } 649 } as server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly server.protocol.DiagnosticWithLinePosition[]; 650 assert.isTrue(diags.length === 0); 651 652 session.executeCommand({ 653 type: "request", 654 command: server.CommandNames.OpenExternalProject, 655 seq: 3, 656 arguments: { 657 projectFileName, 658 rootFiles: externalFiles, 659 options: { module: ModuleKind.CommonJS } 660 } 661 } as server.protocol.OpenExternalProjectRequest); 662 const diagsAfterUpdate = session.executeCommand({ 663 type: "request", 664 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 665 seq: 4, 666 arguments: { projectFileName } 667 } as server.protocol.CompilerOptionsDiagnosticsRequest).response as readonly server.protocol.DiagnosticWithLinePosition[]; 668 assert.isTrue(diagsAfterUpdate.length === 0); 669 }); 670 }); 671 672 describe("unittests:: tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { 673 it("when options change", () => { 674 const file = { 675 path: "/a/b/app.ts", 676 content: "let x = 10" 677 }; 678 const configFileContentBeforeComment = `{`; 679 const configFileContentComment = ` 680 // comment`; 681 const configFileContentAfterComment = ` 682 "compilerOptions": { 683 "inlineSourceMap": true, 684 "mapRoot": "./" 685 } 686 }`; 687 const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; 688 const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; 689 690 const configFile = { 691 path: "/a/b/tsconfig.json", 692 content: configFileContentWithComment 693 }; 694 const host = createServerHost([file, libFile, configFile]); 695 const session = createSession(host); 696 openFilesForSession([file], session); 697 698 const projectService = session.getProjectService(); 699 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 700 const projectName = configuredProjectAt(projectService, 0).getProjectName(); 701 702 const diags = session.executeCommand({ 703 type: "request", 704 command: server.CommandNames.SemanticDiagnosticsSync, 705 seq: 2, 706 arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } 707 } as server.protocol.SemanticDiagnosticsSyncRequest).response as readonly server.protocol.DiagnosticWithLinePosition[]; 708 assert.isTrue(diags.length === 3); 709 710 configFile.content = configFileContentWithoutCommentLine; 711 host.writeFile(configFile.path, configFile.content); 712 713 const diagsAfterEdit = session.executeCommand({ 714 type: "request", 715 command: server.CommandNames.SemanticDiagnosticsSync, 716 seq: 2, 717 arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } 718 } as server.protocol.SemanticDiagnosticsSyncRequest).response as readonly server.protocol.DiagnosticWithLinePosition[]; 719 assert.isTrue(diagsAfterEdit.length === 3); 720 721 verifyDiagnostic(diags[0], diagsAfterEdit[0]); 722 verifyDiagnostic(diags[1], diagsAfterEdit[1]); 723 verifyDiagnostic(diags[2], diagsAfterEdit[2]); 724 725 function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePosition, afterEditDiag: server.protocol.DiagnosticWithLinePosition) { 726 assert.equal(beforeEditDiag.message, afterEditDiag.message); 727 assert.equal(beforeEditDiag.code, afterEditDiag.code); 728 assert.equal(beforeEditDiag.category, afterEditDiag.category); 729 assert.equal(beforeEditDiag.startLocation.line, afterEditDiag.startLocation.line + 1); 730 assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset); 731 assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1); 732 assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset); 733 } 734 }); 735 }); 736 737 describe("unittests:: tsserver:: Project Errors with config file change", () => { 738 it("Updates diagnostics when '--noUnusedLabels' changes", () => { 739 const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; 740 const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; 741 const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; 742 743 const host = createServerHost([aTs, tsconfig]); 744 const session = createSession(host); 745 openFilesForSession([aTs], session); 746 747 host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); 748 host.runQueuedTimeoutCallbacks(); 749 750 const response = executeSessionRequest<protocol.SemanticDiagnosticsSyncRequest, protocol.SemanticDiagnosticsSyncResponse>(session, protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as protocol.Diagnostic[] | undefined; 751 assert.deepEqual<protocol.Diagnostic[] | undefined>(response, [ 752 { 753 start: { line: 1, offset: 1 }, 754 end: { line: 1, offset: 1 + "label".length }, 755 text: "Unused label.", 756 category: "error", 757 code: Diagnostics.Unused_label.code, 758 relatedInformation: undefined, 759 reportsUnnecessary: true, 760 reportsDeprecated: undefined, 761 source: undefined, 762 }, 763 ]); 764 }); 765 }); 766 767 describe("unittests:: tsserver:: Project Errors with resolveJsonModule", () => { 768 function createSessionForTest({ include }: { include: readonly string[]; }) { 769 const test: File = { 770 path: `${tscWatch.projectRoot}/src/test.ts`, 771 content: `import * as blabla from "./blabla.json"; 772declare var console: any; 773console.log(blabla);` 774 }; 775 const blabla: File = { 776 path: `${tscWatch.projectRoot}/src/blabla.json`, 777 content: "{}" 778 }; 779 const tsconfig: File = { 780 path: `${tscWatch.projectRoot}/tsconfig.json`, 781 content: JSON.stringify({ 782 compilerOptions: { 783 resolveJsonModule: true, 784 composite: true 785 }, 786 include 787 }) 788 }; 789 790 const host = createServerHost([test, blabla, libFile, tsconfig]); 791 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 792 openFilesForSession([test], session); 793 return { host, session, test, blabla, tsconfig }; 794 } 795 796 it("should not report incorrect error when json is root file found by tsconfig", () => { 797 const { host, session, test } = createSessionForTest({ 798 include: ["./src/*.ts", "./src/*.json"] 799 }); 800 verifyGetErrRequest({ session, host, files: [test] }); 801 baselineTsserverLogs("projectErrors", `should not report incorrect error when json is root file found by tsconfig`, session); 802 }); 803 804 it("should report error when json is not root file found by tsconfig", () => { 805 const { host, session, test } = createSessionForTest({ 806 include: ["./src/*.ts"] 807 }); 808 verifyGetErrRequest({ session, host, files: [test] }); 809 baselineTsserverLogs("projectErrors", `should report error when json is not root file found by tsconfig`, session); 810 }); 811 }); 812 813 describe("unittests:: tsserver:: Project Errors with npm install when", () => { 814 function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { 815 const main: File = { 816 path: `${tscWatch.projectRoot}/src/main.ts`, 817 content: "import * as _a from '@angular/core';" 818 }; 819 const config: File = { 820 path: `${tscWatch.projectRoot}/tsconfig.json`, 821 content: "{}" 822 }; 823 // Move things from staging to node_modules without triggering watch 824 const moduleFile: File = { 825 path: `${tscWatch.projectRoot}/node_modules/@angular/core/index.d.ts`, 826 content: `export const y = 10;` 827 }; 828 const projectFiles = [main, libFile, config]; 829 const host = createServerHost(projectFiles); 830 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 831 openFilesForSession([{ file: main, projectRootPath: tscWatch.projectRoot }], session); 832 verifyGetErrRequest({ session, host, files: [main] }); 833 834 let npmInstallComplete = false; 835 836 // Simulate npm install 837 let filesAndFoldersToAdd: (File | Folder)[] = [ 838 { path: `${tscWatch.projectRoot}/node_modules` }, // This should queue update 839 { path: `${tscWatch.projectRoot}/node_modules/.staging` }, 840 { path: `${tscWatch.projectRoot}/node_modules/.staging/@babel` }, 841 { path: `${tscWatch.projectRoot}/node_modules/.staging/@babel/helper-plugin-utils-a06c629f` }, 842 { path: `${tscWatch.projectRoot}/node_modules/.staging/core-js-db53158d` }, 843 ]; 844 verifyWhileNpmInstall(3); 845 846 filesAndFoldersToAdd = [ 847 { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/platform-browser-dynamic-5efaaa1a` }, 848 { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/cli-c1e44b05/models/analytics.d.ts`, content: `export const x = 10;` }, 849 { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/core-0963aebf/index.d.ts`, content: `export const y = 10;` }, 850 ]; 851 // Since we added/removed in .staging no timeout 852 verifyWhileNpmInstall(0); 853 854 filesAndFoldersToAdd = []; 855 host.ensureFileOrFolder(moduleFile, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true, /*ignoreParentWatch*/ true); 856 // Since we added/removed in .staging no timeout 857 verifyWhileNpmInstall(0); 858 859 // Remove staging folder to remove errors 860 host.deleteFolder(`${tscWatch.projectRoot}/node_modules/.staging`, /*recursive*/ true); 861 npmInstallComplete = true; 862 projectFiles.push(moduleFile); 863 // Additional watch for watching script infos from node_modules 864 verifyWhileNpmInstall(3); 865 866 baselineTsserverLogs("projectErrors", `npm install when timeout occurs ${timeoutDuringPartialInstallation ? "inbetween" : "after"} installation`, session); 867 868 function verifyWhileNpmInstall(timeouts: number) { 869 filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); 870 if (npmInstallComplete || timeoutDuringPartialInstallation) { 871 host.checkTimeoutQueueLengthAndRun(timeouts); // Invalidation of failed lookups 872 if (timeouts) { 873 host.checkTimeoutQueueLengthAndRun(timeouts - 1); // Actual update 874 } 875 } 876 else { 877 host.checkTimeoutQueueLength(timeouts ? 3 : 2); 878 } 879 verifyGetErrRequest({ session, host, files: [main], existingTimeouts: !npmInstallComplete && !timeoutDuringPartialInstallation ? timeouts ? 3 : 2 : undefined }); 880 } 881 } 882 883 it("timeouts occur inbetween installation", () => { 884 verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); 885 }); 886 887 it("timeout occurs after installation", () => { 888 verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); 889 }); 890 }); 891} 892