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