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 function createErrorLogger() { 205 let hasError = false; 206 const errorLogger: server.Logger = { 207 close: noop, 208 hasLevel: () => true, 209 loggingEnabled: () => true, 210 perftrc: noop, 211 info: noop, 212 msg: (_s, type) => { 213 if (type === server.Msg.Err) { 214 hasError = true; 215 } 216 }, 217 startGroup: noop, 218 endGroup: noop, 219 getLogFileName: returnUndefined 220 }; 221 return { 222 errorLogger, 223 hasError: () => hasError 224 }; 225 } 226 227 it("document is not contained in project", () => { 228 const file1 = { 229 path: "/a/b/app.ts", 230 content: "" 231 }; 232 const corruptedConfig = { 233 path: "/a/b/tsconfig.json", 234 content: "{" 235 }; 236 const host = createServerHost([file1, corruptedConfig]); 237 const projectService = createProjectService(host); 238 239 projectService.openClientFile(file1.path); 240 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 241 242 const project = projectService.findProject(corruptedConfig.path)!; 243 checkProjectRootFiles(project, [file1.path]); 244 }); 245 246 describe("when opening new file that doesnt exist on disk yet", () => { 247 function verifyNonExistentFile(useProjectRoot: boolean) { 248 const folderPath = "/user/someuser/projects/someFolder"; 249 const fileInRoot: File = { 250 path: `/src/somefile.d.ts`, 251 content: "class c { }" 252 }; 253 const fileInProjectRoot: File = { 254 path: `${folderPath}/src/somefile.d.ts`, 255 content: "class c { }" 256 }; 257 const host = createServerHost([libFile, fileInRoot, fileInProjectRoot]); 258 const { hasError, errorLogger } = createErrorLogger(); 259 const session = createSession(host, { canUseEvents: true, logger: errorLogger, useInferredProjectPerProjectRoot: true }); 260 261 const projectService = session.getProjectService(); 262 const untitledFile = "untitled:Untitled-1"; 263 const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; 264 const refPathNotFound2 = "./src/somefile.d.ts"; 265 const fileContent = `/// <reference path="${refPathNotFound1}" /> 266/// <reference path="${refPathNotFound2}" />`; 267 session.executeCommandSeq<protocol.OpenRequest>({ 268 command: server.CommandNames.Open, 269 arguments: { 270 file: untitledFile, 271 fileContent, 272 scriptKindName: "TS", 273 projectRootPath: useProjectRoot ? folderPath : undefined 274 } 275 }); 276 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 277 const infoForUntitledAtProjectRoot = projectService.getScriptInfoForPath(`${folderPath.toLowerCase()}/${untitledFile.toLowerCase()}` as Path); 278 const infoForUnitiledAtRoot = projectService.getScriptInfoForPath(`/${untitledFile.toLowerCase()}` as Path); 279 const infoForSomefileAtProjectRoot = projectService.getScriptInfoForPath(`/${folderPath.toLowerCase()}/src/somefile.d.ts` as Path); 280 const infoForSomefileAtRoot = projectService.getScriptInfoForPath(`${fileInRoot.path.toLowerCase()}` as Path); 281 if (useProjectRoot) { 282 assert.isDefined(infoForUntitledAtProjectRoot); 283 assert.isUndefined(infoForUnitiledAtRoot); 284 } 285 else { 286 assert.isDefined(infoForUnitiledAtRoot); 287 assert.isUndefined(infoForUntitledAtProjectRoot); 288 } 289 assert.isUndefined(infoForSomefileAtRoot); 290 assert.isUndefined(infoForSomefileAtProjectRoot); 291 292 // Since this is not js project so no typings are queued 293 host.checkTimeoutQueueLength(0); 294 295 const errorOffset = fileContent.indexOf(refPathNotFound1) + 1; 296 verifyGetErrRequest({ 297 session, 298 host, 299 expected: [{ 300 file: untitledFile, 301 syntax: [], 302 semantic: [ 303 createDiagnostic({ line: 1, offset: errorOffset }, { line: 1, offset: errorOffset + refPathNotFound1.length }, Diagnostics.File_0_not_found, [refPathNotFound1], "error"), 304 createDiagnostic({ line: 2, offset: errorOffset }, { line: 2, offset: errorOffset + refPathNotFound2.length }, Diagnostics.File_0_not_found, [refPathNotFound2.substr(2)], "error") 305 ], 306 suggestion: [] 307 }], 308 onErrEvent: () => assert.isFalse(hasError()) 309 }); 310 } 311 312 it("has projectRoot", () => { 313 verifyNonExistentFile(/*useProjectRoot*/ true); 314 }); 315 316 it("does not have projectRoot", () => { 317 verifyNonExistentFile(/*useProjectRoot*/ false); 318 }); 319 }); 320 321 it("folder rename updates project structure and reports no errors", () => { 322 const projectDir = "/a/b/projects/myproject"; 323 const app: File = { 324 path: `${projectDir}/bar/app.ts`, 325 content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" 326 }; 327 const foo: File = { 328 path: `${projectDir}/foo/foo.ts`, 329 content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" 330 }; 331 const configFile: File = { 332 path: `${projectDir}/tsconfig.json`, 333 content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) 334 }; 335 const host = createServerHost([app, foo, configFile]); 336 const session = createSession(host, { canUseEvents: true, }); 337 const projectService = session.getProjectService(); 338 339 session.executeCommandSeq<protocol.OpenRequest>({ 340 command: server.CommandNames.Open, 341 arguments: { file: app.path, } 342 }); 343 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 344 assert.isDefined(projectService.configuredProjects.get(configFile.path)); 345 verifyErrorsInApp(); 346 347 host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); 348 host.runQueuedTimeoutCallbacks(); 349 host.runQueuedTimeoutCallbacks(); 350 verifyErrorsInApp(); 351 352 function verifyErrorsInApp() { 353 verifyGetErrRequestNoErrors({ session, host, files: [app] }); 354 } 355 }); 356 357 it("Getting errors before opening file", () => { 358 const file: File = { 359 path: "/a/b/project/file.ts", 360 content: "let x: number = false;" 361 }; 362 const host = createServerHost([file, libFile]); 363 const { hasError, errorLogger } = createErrorLogger(); 364 const session = createSession(host, { canUseEvents: true, logger: errorLogger }); 365 366 session.clearMessages(); 367 const expectedSequenceId = session.getNextSeq(); 368 session.executeCommandSeq<protocol.GeterrRequest>({ 369 command: server.CommandNames.Geterr, 370 arguments: { 371 delay: 0, 372 files: [file.path] 373 } 374 }); 375 376 host.checkTimeoutQueueLengthAndRun(1); 377 assert.isFalse(hasError()); 378 checkCompleteEvent(session, 1, expectedSequenceId); 379 session.clearMessages(); 380 }); 381 382 it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { 383 const app: File = { 384 path: `${tscWatch.projectRoot}/src/client/app.js`, 385 content: "" 386 }; 387 const serverUtilities: File = { 388 path: `${tscWatch.projectRoot}/src/server/utilities.js`, 389 content: `function getHostName() { return "hello"; } export { getHostName };` 390 }; 391 const backendTest: File = { 392 path: `${tscWatch.projectRoot}/test/backend/index.js`, 393 content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` 394 }; 395 const files = [libFile, app, serverUtilities, backendTest]; 396 const host = createServerHost(files); 397 const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true }); 398 openFilesForSession([{ file: app, projectRootPath: tscWatch.projectRoot }], session); 399 const service = session.getProjectService(); 400 checkNumberOfProjects(service, { inferredProjects: 1 }); 401 const project = service.inferredProjects[0]; 402 checkProjectActualFiles(project, [libFile.path, app.path]); 403 openFilesForSession([{ file: backendTest, projectRootPath: tscWatch.projectRoot }], session); 404 checkNumberOfProjects(service, { inferredProjects: 1 }); 405 checkProjectActualFiles(project, files.map(f => f.path)); 406 checkErrors([backendTest.path, app.path]); 407 closeFilesForSession([backendTest], session); 408 openFilesForSession([{ file: serverUtilities.path, projectRootPath: tscWatch.projectRoot }], session); 409 checkErrors([serverUtilities.path, app.path]); 410 411 function checkErrors(openFiles: [string, string]) { 412 verifyGetErrRequestNoErrors({ session, host, files: openFiles }); 413 } 414 }); 415 416 it("Correct errors when resolution resolves to file that has same ambient module and is also module", () => { 417 const projectRootPath = "/users/username/projects/myproject"; 418 const aFile: File = { 419 path: `${projectRootPath}/src/a.ts`, 420 content: `import * as myModule from "@custom/plugin"; 421function foo() { 422 // hello 423}` 424 }; 425 const config: File = { 426 path: `${projectRootPath}/tsconfig.json`, 427 content: JSON.stringify({ include: ["src"] }) 428 }; 429 const plugin: File = { 430 path: `${projectRootPath}/node_modules/@custom/plugin/index.d.ts`, 431 content: `import './proposed'; 432declare module '@custom/plugin' { 433 export const version: string; 434}` 435 }; 436 const pluginProposed: File = { 437 path: `${projectRootPath}/node_modules/@custom/plugin/proposed.d.ts`, 438 content: `declare module '@custom/plugin' { 439 export const bar = 10; 440}` 441 }; 442 const files = [libFile, aFile, config, plugin, pluginProposed]; 443 const host = createServerHost(files); 444 const session = createSession(host, { canUseEvents: true }); 445 const service = session.getProjectService(); 446 openFilesForSession([aFile], session); 447 448 checkNumberOfProjects(service, { configuredProjects: 1 }); 449 session.clearMessages(); 450 checkErrors(); 451 452 session.executeCommandSeq<protocol.ChangeRequest>({ 453 command: protocol.CommandTypes.Change, 454 arguments: { 455 file: aFile.path, 456 line: 3, 457 offset: 8, 458 endLine: 3, 459 endOffset: 8, 460 insertString: "o" 461 } 462 }); 463 checkErrors(); 464 465 function checkErrors() { 466 host.checkTimeoutQueueLength(0); 467 verifyGetErrRequest({ 468 session, 469 host, 470 expected: [{ 471 file: aFile, 472 syntax: [], 473 semantic: [], 474 suggestion: [ 475 createDiagnostic({ line: 1, offset: 1 }, { line: 1, offset: 44 }, Diagnostics._0_is_declared_but_its_value_is_never_read, ["myModule"], "suggestion", /*reportsUnnecessary*/ true), 476 createDiagnostic({ line: 2, offset: 10 }, { line: 2, offset: 13 }, Diagnostics._0_is_declared_but_its_value_is_never_read, ["foo"], "suggestion", /*reportsUnnecessary*/ true) 477 ] 478 }] 479 }); 480 } 481 }); 482 483 describe("when semantic error returns includes global error", () => { 484 const file: File = { 485 path: `${tscWatch.projectRoot}/ui.ts`, 486 content: `const x = async (_action: string) => { 487};` 488 }; 489 const config: File = { 490 path: `${tscWatch.projectRoot}/tsconfig.json`, 491 content: "{}" 492 }; 493 function expectedDiagnostics(): GetErrDiagnostics { 494 const span = protocolTextSpanFromSubstring(file.content, `async (_action: string) => {`); 495 return { 496 file, 497 syntax: [], 498 semantic: [ 499 createDiagnostic(span.start, span.end, Diagnostics.An_async_function_or_method_must_return_a_Promise_Make_sure_you_have_a_declaration_for_Promise_or_include_ES2015_in_your_lib_option, [], "error"), 500 ], 501 suggestion: [] 502 }; 503 } 504 verifyGetErrScenario({ 505 allFiles: () => [libFile, file, config], 506 openFiles: () => [file], 507 expectedGetErr: () => [expectedDiagnostics()], 508 expectedGetErrForProject: () => [{ 509 project: file.path, 510 errors: [ 511 expectedDiagnostics(), 512 ] 513 }], 514 expectedSyncDiagnostics: () => [ 515 syncDiagnostics(expectedDiagnostics(), config.path), 516 ], 517 expectedConfigFileDiagEvents: () => [{ 518 triggerFile: file.path, 519 configFileName: config.path, 520 diagnostics: emptyArray 521 }] 522 }); 523 }); 524 }); 525 526 describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { 527 function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string, didYouMean?: string): ConfigFileDiagnostic { 528 const d = didYouMean ? Diagnostics.Unknown_compiler_option_0_Did_you_mean_1 : Diagnostics.Unknown_compiler_option_0; 529 const start = configFile.content.indexOf(prop) - 1; // start at "prop" 530 return { 531 fileName: configFile.path, 532 start, 533 length: prop.length + 2, 534 messageText: formatStringFromArgs(d.message, didYouMean ? [prop, didYouMean] : [prop]), 535 category: d.category, 536 code: d.code, 537 reportsUnnecessary: undefined, 538 reportsDeprecated: undefined 539 }; 540 } 541 542 function getFileNotFoundDiagnostic(configFile: File, relativeFileName: string): ConfigFileDiagnostic { 543 const findString = `{"path":"./${relativeFileName}"}`; 544 const d = Diagnostics.File_0_not_found; 545 const start = configFile.content.indexOf(findString); 546 return { 547 fileName: configFile.path, 548 start, 549 length: findString.length, 550 messageText: formatStringFromArgs(d.message, [`${getDirectoryPath(configFile.path)}/${relativeFileName}`]), 551 category: d.category, 552 code: d.code, 553 reportsUnnecessary: undefined, 554 reportsDeprecated: undefined 555 }; 556 } 557 558 it("are generated when the config file has errors", () => { 559 const file: File = { 560 path: "/a/b/app.ts", 561 content: "let x = 10" 562 }; 563 const configFile: File = { 564 path: "/a/b/tsconfig.json", 565 content: `{ 566 "compilerOptions": { 567 "foo": "bar", 568 "allowJS": true 569 } 570 }` 571 }; 572 const serverEventManager = new TestServerEventManager([file, libFile, configFile]); 573 openFilesForSession([file], serverEventManager.session); 574 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ 575 getUnknownCompilerOptionDiagnostic(configFile, "foo"), 576 getUnknownCompilerOptionDiagnostic(configFile, "allowJS", "allowJs") 577 ]); 578 }); 579 580 it("are generated when the config file doesn't have errors", () => { 581 const file: File = { 582 path: "/a/b/app.ts", 583 content: "let x = 10" 584 }; 585 const configFile: File = { 586 path: "/a/b/tsconfig.json", 587 content: `{ 588 "compilerOptions": {} 589 }` 590 }; 591 const serverEventManager = new TestServerEventManager([file, libFile, configFile]); 592 openFilesForSession([file], serverEventManager.session); 593 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); 594 }); 595 596 it("are generated when the config file changes", () => { 597 const file: File = { 598 path: "/a/b/app.ts", 599 content: "let x = 10" 600 }; 601 const configFile = { 602 path: "/a/b/tsconfig.json", 603 content: `{ 604 "compilerOptions": {} 605 }` 606 }; 607 608 const files = [file, libFile, configFile]; 609 const serverEventManager = new TestServerEventManager(files); 610 openFilesForSession([file], serverEventManager.session); 611 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); 612 613 configFile.content = `{ 614 "compilerOptions": { 615 "haha": 123 616 } 617 }`; 618 serverEventManager.host.writeFile(configFile.path, configFile.content); 619 serverEventManager.host.runQueuedTimeoutCallbacks(); 620 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, [ 621 getUnknownCompilerOptionDiagnostic(configFile, "haha") 622 ]); 623 624 configFile.content = `{ 625 "compilerOptions": {} 626 }`; 627 serverEventManager.host.writeFile(configFile.path, configFile.content); 628 serverEventManager.host.runQueuedTimeoutCallbacks(); 629 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, emptyArray); 630 }); 631 632 it("are not generated when the config file does not include file opened and config file has errors", () => { 633 const file: File = { 634 path: "/a/b/app.ts", 635 content: "let x = 10" 636 }; 637 const file2: File = { 638 path: "/a/b/test.ts", 639 content: "let x = 10" 640 }; 641 const file3: File = { 642 path: "/a/b/test2.ts", 643 content: "let xy = 10" 644 }; 645 const configFile: File = { 646 path: "/a/b/tsconfig.json", 647 content: `{ 648 "compilerOptions": { 649 "foo": "bar", 650 "allowJS": true 651 }, 652 "files": ["app.ts"] 653 }` 654 }; 655 const serverEventManager = new TestServerEventManager([file, file2, file3, libFile, configFile]); 656 openFilesForSession([file2], serverEventManager.session); 657 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file2.path, [ 658 getUnknownCompilerOptionDiagnostic(configFile, "foo"), 659 getUnknownCompilerOptionDiagnostic(configFile, "allowJS", "allowJs") 660 ]); 661 openFilesForSession([file], serverEventManager.session); 662 // We generate only if project is created when opening file from the project 663 serverEventManager.hasZeroEvent("configFileDiag"); 664 openFilesForSession([file3], serverEventManager.session); 665 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file3.path, [ 666 getUnknownCompilerOptionDiagnostic(configFile, "foo"), 667 getUnknownCompilerOptionDiagnostic(configFile, "allowJS", "allowJs") 668 ]); 669 }); 670 671 it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { 672 const file: File = { 673 path: "/a/b/app.ts", 674 content: "let x = 10" 675 }; 676 const configFile: File = { 677 path: "/a/b/tsconfig.json", 678 content: `{ 679 "compilerOptions": { 680 "foo": "bar", 681 "allowJS": true 682 } 683 }` 684 }; 685 const serverEventManager = new TestServerEventManager([file, libFile, configFile], /*suppressDiagnosticEvents*/ true); 686 openFilesForSession([file], serverEventManager.session); 687 serverEventManager.hasZeroEvent("configFileDiag"); 688 }); 689 690 it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { 691 const file: File = { 692 path: "/a/b/app.ts", 693 content: "let x = 10" 694 }; 695 const file2: File = { 696 path: "/a/b/test.ts", 697 content: "let x = 10" 698 }; 699 const file3: File = { 700 path: "/a/b/test2.ts", 701 content: "let xy = 10" 702 }; 703 const configFile: File = { 704 path: "/a/b/tsconfig.json", 705 content: `{ 706 "files": ["app.ts"] 707 }` 708 }; 709 710 const serverEventManager = new TestServerEventManager([file, file2, file3, libFile, configFile]); 711 openFilesForSession([file2], serverEventManager.session); 712 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file2.path, emptyArray); 713 openFilesForSession([file], serverEventManager.session); 714 // We generate only if project is created when opening file from the project 715 serverEventManager.hasZeroEvent("configFileDiag"); 716 openFilesForSession([file3], serverEventManager.session); 717 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file3.path, emptyArray); 718 }); 719 720 it("contains the project reference errors", () => { 721 const file: File = { 722 path: "/a/b/app.ts", 723 content: "let x = 10" 724 }; 725 const noSuchTsconfig = "no-such-tsconfig.json"; 726 const configFile: File = { 727 path: "/a/b/tsconfig.json", 728 content: `{ 729 "files": ["app.ts"], 730 "references": [{"path":"./${noSuchTsconfig}"}] 731 }` 732 }; 733 734 const serverEventManager = new TestServerEventManager([file, libFile, configFile]); 735 openFilesForSession([file], serverEventManager.session); 736 serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ 737 getFileNotFoundDiagnostic(configFile, noSuchTsconfig) 738 ]); 739 }); 740 }); 741 742 describe("unittests:: tsserver:: Project Errors dont include overwrite emit error", () => { 743 it("for inferred project", () => { 744 const f1 = { 745 path: "/a/b/f1.js", 746 content: "function test1() { }" 747 }; 748 const host = createServerHost([f1, libFile]); 749 const session = createSession(host); 750 openFilesForSession([f1], session); 751 752 const projectService = session.getProjectService(); 753 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 754 const projectName = projectService.inferredProjects[0].getProjectName(); 755 756 const diags = session.executeCommand(<server.protocol.CompilerOptionsDiagnosticsRequest>{ 757 type: "request", 758 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 759 seq: 2, 760 arguments: { projectFileName: projectName } 761 }).response as readonly protocol.DiagnosticWithLinePosition[]; 762 assert.isTrue(diags.length === 0); 763 764 session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{ 765 type: "request", 766 command: server.CommandNames.CompilerOptionsForInferredProjects, 767 seq: 3, 768 arguments: { options: { module: ModuleKind.CommonJS } } 769 }); 770 const diagsAfterUpdate = session.executeCommand(<server.protocol.CompilerOptionsDiagnosticsRequest>{ 771 type: "request", 772 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 773 seq: 4, 774 arguments: { projectFileName: projectName } 775 }).response as readonly protocol.DiagnosticWithLinePosition[]; 776 assert.isTrue(diagsAfterUpdate.length === 0); 777 }); 778 779 it("for external project", () => { 780 const f1 = { 781 path: "/a/b/f1.js", 782 content: "function test1() { }" 783 }; 784 const host = createServerHost([f1, libFile]); 785 const session = createSession(host); 786 const projectService = session.getProjectService(); 787 const projectFileName = "/a/b/project.csproj"; 788 const externalFiles = toExternalFiles([f1.path]); 789 projectService.openExternalProject(<protocol.ExternalProject>{ 790 projectFileName, 791 rootFiles: externalFiles, 792 options: {} 793 }); 794 795 checkNumberOfProjects(projectService, { externalProjects: 1 }); 796 797 const diags = session.executeCommand(<server.protocol.CompilerOptionsDiagnosticsRequest>{ 798 type: "request", 799 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 800 seq: 2, 801 arguments: { projectFileName } 802 }).response as readonly server.protocol.DiagnosticWithLinePosition[]; 803 assert.isTrue(diags.length === 0); 804 805 session.executeCommand(<server.protocol.OpenExternalProjectRequest>{ 806 type: "request", 807 command: server.CommandNames.OpenExternalProject, 808 seq: 3, 809 arguments: { 810 projectFileName, 811 rootFiles: externalFiles, 812 options: { module: ModuleKind.CommonJS } 813 } 814 }); 815 const diagsAfterUpdate = session.executeCommand(<server.protocol.CompilerOptionsDiagnosticsRequest>{ 816 type: "request", 817 command: server.CommandNames.CompilerOptionsDiagnosticsFull, 818 seq: 4, 819 arguments: { projectFileName } 820 }).response as readonly server.protocol.DiagnosticWithLinePosition[]; 821 assert.isTrue(diagsAfterUpdate.length === 0); 822 }); 823 }); 824 825 describe("unittests:: tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { 826 it("when options change", () => { 827 const file = { 828 path: "/a/b/app.ts", 829 content: "let x = 10" 830 }; 831 const configFileContentBeforeComment = `{`; 832 const configFileContentComment = ` 833 // comment`; 834 const configFileContentAfterComment = ` 835 "compilerOptions": { 836 "inlineSourceMap": true, 837 "mapRoot": "./" 838 } 839 }`; 840 const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; 841 const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; 842 843 const configFile = { 844 path: "/a/b/tsconfig.json", 845 content: configFileContentWithComment 846 }; 847 const host = createServerHost([file, libFile, configFile]); 848 const session = createSession(host); 849 openFilesForSession([file], session); 850 851 const projectService = session.getProjectService(); 852 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 853 const projectName = configuredProjectAt(projectService, 0).getProjectName(); 854 855 const diags = session.executeCommand(<server.protocol.SemanticDiagnosticsSyncRequest>{ 856 type: "request", 857 command: server.CommandNames.SemanticDiagnosticsSync, 858 seq: 2, 859 arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } 860 }).response as readonly server.protocol.DiagnosticWithLinePosition[]; 861 assert.isTrue(diags.length === 3); 862 863 configFile.content = configFileContentWithoutCommentLine; 864 host.writeFile(configFile.path, configFile.content); 865 866 const diagsAfterEdit = session.executeCommand(<server.protocol.SemanticDiagnosticsSyncRequest>{ 867 type: "request", 868 command: server.CommandNames.SemanticDiagnosticsSync, 869 seq: 2, 870 arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } 871 }).response as readonly server.protocol.DiagnosticWithLinePosition[]; 872 assert.isTrue(diagsAfterEdit.length === 3); 873 874 verifyDiagnostic(diags[0], diagsAfterEdit[0]); 875 verifyDiagnostic(diags[1], diagsAfterEdit[1]); 876 verifyDiagnostic(diags[2], diagsAfterEdit[2]); 877 878 function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePosition, afterEditDiag: server.protocol.DiagnosticWithLinePosition) { 879 assert.equal(beforeEditDiag.message, afterEditDiag.message); 880 assert.equal(beforeEditDiag.code, afterEditDiag.code); 881 assert.equal(beforeEditDiag.category, afterEditDiag.category); 882 assert.equal(beforeEditDiag.startLocation.line, afterEditDiag.startLocation.line + 1); 883 assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset); 884 assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1); 885 assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset); 886 } 887 }); 888 }); 889 890 describe("unittests:: tsserver:: Project Errors with config file change", () => { 891 it("Updates diagnostics when '--noUnusedLabels' changes", () => { 892 const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; 893 const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; 894 const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; 895 896 const host = createServerHost([aTs, tsconfig]); 897 const session = createSession(host); 898 openFilesForSession([aTs], session); 899 900 host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); 901 host.runQueuedTimeoutCallbacks(); 902 903 const response = executeSessionRequest<protocol.SemanticDiagnosticsSyncRequest, protocol.SemanticDiagnosticsSyncResponse>(session, protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as protocol.Diagnostic[] | undefined; 904 assert.deepEqual<protocol.Diagnostic[] | undefined>(response, [ 905 { 906 start: { line: 1, offset: 1 }, 907 end: { line: 1, offset: 1 + "label".length }, 908 text: "Unused label.", 909 category: "error", 910 code: Diagnostics.Unused_label.code, 911 relatedInformation: undefined, 912 reportsUnnecessary: true, 913 reportsDeprecated: undefined, 914 source: undefined, 915 }, 916 ]); 917 }); 918 }); 919 920 describe("unittests:: tsserver:: Project Errors with resolveJsonModule", () => { 921 function createSessionForTest({ include }: { include: readonly string[]; }) { 922 const test: File = { 923 path: `${tscWatch.projectRoot}/src/test.ts`, 924 content: `import * as blabla from "./blabla.json"; 925declare var console: any; 926console.log(blabla);` 927 }; 928 const blabla: File = { 929 path: `${tscWatch.projectRoot}/src/blabla.json`, 930 content: "{}" 931 }; 932 const tsconfig: File = { 933 path: `${tscWatch.projectRoot}/tsconfig.json`, 934 content: JSON.stringify({ 935 compilerOptions: { 936 resolveJsonModule: true, 937 composite: true 938 }, 939 include 940 }) 941 }; 942 943 const host = createServerHost([test, blabla, libFile, tsconfig]); 944 const session = createSession(host, { canUseEvents: true }); 945 openFilesForSession([test], session); 946 return { host, session, test, blabla, tsconfig }; 947 } 948 949 it("should not report incorrect error when json is root file found by tsconfig", () => { 950 const { host, session, test } = createSessionForTest({ 951 include: ["./src/*.ts", "./src/*.json"] 952 }); 953 verifyGetErrRequestNoErrors({ session, host, files: [test] }); 954 }); 955 956 it("should report error when json is not root file found by tsconfig", () => { 957 const { host, session, test, blabla, tsconfig } = createSessionForTest({ 958 include: ["./src/*.ts"] 959 }); 960 const span = protocolTextSpanFromSubstring(test.content, `"./blabla.json"`); 961 verifyGetErrRequest({ 962 session, 963 host, 964 expected: [{ 965 file: test, 966 syntax: [], 967 semantic: [ 968 createDiagnostic( 969 span.start, 970 span.end, 971 Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, 972 [blabla.path, tsconfig.path] 973 ) 974 ], 975 suggestion: [] 976 }] 977 }); 978 }); 979 }); 980 981 describe("unittests:: tsserver:: Project Errors with npm install when", () => { 982 function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { 983 const main: File = { 984 path: `${tscWatch.projectRoot}/src/main.ts`, 985 content: "import * as _a from '@angular/core';" 986 }; 987 const config: File = { 988 path: `${tscWatch.projectRoot}/tsconfig.json`, 989 content: "{}" 990 }; 991 // Move things from staging to node_modules without triggering watch 992 const moduleFile: File = { 993 path: `${tscWatch.projectRoot}/node_modules/@angular/core/index.d.ts`, 994 content: `export const y = 10;` 995 }; 996 const projectFiles = [main, libFile, config]; 997 const host = createServerHost(projectFiles); 998 const session = createSession(host, { canUseEvents: true }); 999 const service = session.getProjectService(); 1000 openFilesForSession([{ file: main, projectRootPath: tscWatch.projectRoot }], session); 1001 const span = protocolTextSpanFromSubstring(main.content, `'@angular/core'`); 1002 const moduleNotFoundErr: protocol.Diagnostic[] = [ 1003 createDiagnostic( 1004 span.start, 1005 span.end, 1006 Diagnostics.Cannot_find_module_0_or_its_corresponding_type_declarations, 1007 ["@angular/core"] 1008 ) 1009 ]; 1010 const expectedRecursiveWatches = arrayToMap([`${tscWatch.projectRoot}`, `${tscWatch.projectRoot}/src`, `${tscWatch.projectRoot}/node_modules`, `${tscWatch.projectRoot}/node_modules/@types`], identity, () => 1); 1011 verifyProject(); 1012 verifyErrors(moduleNotFoundErr); 1013 1014 let npmInstallComplete = false; 1015 1016 // Simulate npm install 1017 let filesAndFoldersToAdd: (File | Folder)[] = [ 1018 { path: `${tscWatch.projectRoot}/node_modules` }, // This should queue update 1019 { path: `${tscWatch.projectRoot}/node_modules/.staging` }, 1020 { path: `${tscWatch.projectRoot}/node_modules/.staging/@babel` }, 1021 { path: `${tscWatch.projectRoot}/node_modules/.staging/@babel/helper-plugin-utils-a06c629f` }, 1022 { path: `${tscWatch.projectRoot}/node_modules/.staging/core-js-db53158d` }, 1023 ]; 1024 verifyWhileNpmInstall({ timeouts: 3, semantic: moduleNotFoundErr }); 1025 1026 filesAndFoldersToAdd = [ 1027 { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/platform-browser-dynamic-5efaaa1a` }, 1028 { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/cli-c1e44b05/models/analytics.d.ts`, content: `export const x = 10;` }, 1029 { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/core-0963aebf/index.d.ts`, content: `export const y = 10;` }, 1030 ]; 1031 // Since we added/removed in .staging no timeout 1032 verifyWhileNpmInstall({ timeouts: 0, semantic: moduleNotFoundErr }); 1033 1034 filesAndFoldersToAdd = []; 1035 host.ensureFileOrFolder(moduleFile, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true, /*ignoreParentWatch*/ true); 1036 // Since we added/removed in .staging no timeout 1037 verifyWhileNpmInstall({ timeouts: 0, semantic: moduleNotFoundErr }); 1038 1039 // Remove staging folder to remove errors 1040 host.deleteFolder(`${tscWatch.projectRoot}/node_modules/.staging`, /*recursive*/ true); 1041 npmInstallComplete = true; 1042 projectFiles.push(moduleFile); 1043 // Additional watch for watching script infos from node_modules 1044 expectedRecursiveWatches.set(`${tscWatch.projectRoot}/node_modules`, 2); 1045 verifyWhileNpmInstall({ timeouts: 3, semantic: [] }); 1046 1047 function verifyWhileNpmInstall({ timeouts, semantic }: { timeouts: number; semantic: protocol.Diagnostic[] }) { 1048 filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); 1049 if (npmInstallComplete || timeoutDuringPartialInstallation) { 1050 host.checkTimeoutQueueLengthAndRun(timeouts); // Invalidation of failed lookups 1051 if (timeouts) { 1052 host.checkTimeoutQueueLengthAndRun(timeouts - 1); // Actual update 1053 } 1054 } 1055 else { 1056 host.checkTimeoutQueueLength(timeouts ? 3 : 2); 1057 } 1058 verifyProject(); 1059 verifyErrors(semantic, !npmInstallComplete && !timeoutDuringPartialInstallation ? timeouts ? 3 : 2 : undefined); 1060 } 1061 1062 function verifyProject() { 1063 checkNumberOfConfiguredProjects(service, 1); 1064 1065 const project = service.configuredProjects.get(config.path)!; 1066 checkProjectActualFiles(project, map(projectFiles, f => f.path)); 1067 1068 checkWatchedFilesDetailed(host, mapDefined(projectFiles, f => f === main || f === moduleFile ? undefined : f.path), 1); 1069 checkWatchedDirectoriesDetailed(host, expectedRecursiveWatches, /*recursive*/ true); 1070 checkWatchedDirectories(host, [], /*recursive*/ false); 1071 } 1072 1073 function verifyErrors(semantic: protocol.Diagnostic[], existingTimeouts?: number) { 1074 verifyGetErrRequest({ 1075 session, 1076 host, 1077 expected: [{ 1078 file: main, 1079 syntax: [], 1080 semantic, 1081 suggestion: [] 1082 }], 1083 existingTimeouts 1084 }); 1085 1086 } 1087 } 1088 1089 it("timeouts occur inbetween installation", () => { 1090 verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); 1091 }); 1092 1093 it("timeout occurs after installation", () => { 1094 verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); 1095 }); 1096 }); 1097} 1098