• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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