• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts {
2
3    const enum ChangedPart {
4        references = 1 << 0,
5        importsAndExports = 1 << 1,
6        program = 1 << 2
7    }
8
9    const newLine = "\r\n";
10
11    interface SourceFileWithText extends SourceFile {
12        sourceText?: SourceText;
13    }
14
15    export interface NamedSourceText {
16        name: string;
17        text: SourceText;
18    }
19
20    export interface ProgramWithSourceTexts extends Program {
21        sourceTexts?: readonly NamedSourceText[];
22        host: TestCompilerHost;
23    }
24
25    interface TestCompilerHost extends CompilerHost {
26        getTrace(): string[];
27    }
28
29    export class SourceText implements IScriptSnapshot {
30        private fullText: string | undefined;
31
32        constructor(private references: string,
33            private importsAndExports: string,
34            private program: string,
35            private changedPart: ChangedPart = 0,
36            private version = 0) {
37        }
38
39        static New(references: string, importsAndExports: string, program: string): SourceText {
40            Debug.assert(references !== undefined);
41            Debug.assert(importsAndExports !== undefined);
42            Debug.assert(program !== undefined);
43            return new SourceText(references + newLine, importsAndExports + newLine, program || "");
44        }
45
46        public getVersion(): number {
47            return this.version;
48        }
49
50        public updateReferences(newReferences: string): SourceText {
51            Debug.assert(newReferences !== undefined);
52            return new SourceText(newReferences + newLine, this.importsAndExports, this.program, this.changedPart | ChangedPart.references, this.version + 1);
53        }
54        public updateImportsAndExports(newImportsAndExports: string): SourceText {
55            Debug.assert(newImportsAndExports !== undefined);
56            return new SourceText(this.references, newImportsAndExports + newLine, this.program, this.changedPart | ChangedPart.importsAndExports, this.version + 1);
57        }
58        public updateProgram(newProgram: string): SourceText {
59            Debug.assert(newProgram !== undefined);
60            return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1);
61        }
62
63        public getFullText() {
64            return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program);
65        }
66
67        public getText(start: number, end: number): string {
68            return this.getFullText().substring(start, end);
69        }
70
71        getLength(): number {
72            return this.getFullText().length;
73        }
74
75        getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange {
76            const oldText = <SourceText>oldSnapshot;
77            let oldSpan: TextSpan;
78            let newLength: number;
79            switch (oldText.changedPart ^ this.changedPart) {
80                case ChangedPart.references:
81                    oldSpan = createTextSpan(0, oldText.references.length);
82                    newLength = this.references.length;
83                    break;
84                case ChangedPart.importsAndExports:
85                    oldSpan = createTextSpan(oldText.references.length, oldText.importsAndExports.length);
86                    newLength = this.importsAndExports.length;
87                    break;
88                case ChangedPart.program:
89                    oldSpan = createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length);
90                    newLength = this.program.length;
91                    break;
92                default:
93                    return Debug.fail("Unexpected change");
94            }
95
96            return createTextChangeRange(oldSpan, newLength);
97        }
98    }
99
100    function createSourceFileWithText(fileName: string, sourceText: SourceText, target: ScriptTarget) {
101        const file = <SourceFileWithText>createSourceFile(fileName, sourceText.getFullText(), target);
102        file.sourceText = sourceText;
103        file.version = "" + sourceText.getVersion();
104        return file;
105    }
106
107    export function createTestCompilerHost(texts: readonly NamedSourceText[], target: ScriptTarget, oldProgram?: ProgramWithSourceTexts, useGetSourceFileByPath?: boolean) {
108        const files = arrayToMap(texts, t => t.name, t => {
109            if (oldProgram) {
110                let oldFile = <SourceFileWithText>oldProgram.getSourceFile(t.name);
111                if (oldFile && oldFile.redirectInfo) {
112                    oldFile = oldFile.redirectInfo.unredirected;
113                }
114                if (oldFile && oldFile.sourceText!.getVersion() === t.text.getVersion()) {
115                    return oldFile;
116                }
117            }
118            return createSourceFileWithText(t.name, t.text, target);
119        });
120        const useCaseSensitiveFileNames = sys && sys.useCaseSensitiveFileNames;
121        const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
122        const trace: string[] = [];
123        const result: TestCompilerHost = {
124            trace: s => trace.push(s),
125            getTrace: () => trace,
126            getSourceFile: fileName => files.get(fileName),
127            getDefaultLibFileName: () => "lib.d.ts",
128            writeFile: notImplemented,
129            getCurrentDirectory: () => "",
130            getDirectories: () => [],
131            getCanonicalFileName,
132            useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
133            getNewLine: () => sys ? sys.newLine : newLine,
134            fileExists: fileName => files.has(fileName),
135            readFile: fileName => {
136                const file = files.get(fileName);
137                return file && file.text;
138            },
139        };
140        if (useGetSourceFileByPath) {
141            const filesByPath = mapEntries(files, (fileName, file) => [toPath(fileName, "", getCanonicalFileName), file]);
142            result.getSourceFileByPath = (_fileName, path) => filesByPath.get(path);
143        }
144        return result;
145    }
146
147    export function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions, useGetSourceFileByPath?: boolean): ProgramWithSourceTexts {
148        const host = createTestCompilerHost(texts, options.target!, /*oldProgram*/ undefined, useGetSourceFileByPath);
149        const program = <ProgramWithSourceTexts>createProgram(rootNames, options, host);
150        program.sourceTexts = texts;
151        program.host = host;
152        return program;
153    }
154
155    export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: readonly string[], options: CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[], useGetSourceFileByPath?: boolean) {
156        if (!newTexts) {
157            newTexts = oldProgram.sourceTexts!.slice(0);
158        }
159        updater(newTexts);
160        const host = createTestCompilerHost(newTexts, options.target!, oldProgram, useGetSourceFileByPath);
161        const program = <ProgramWithSourceTexts>createProgram(rootNames, options, host, oldProgram);
162        program.sourceTexts = newTexts;
163        program.host = host;
164        return program;
165    }
166
167    export function updateProgramText(files: readonly NamedSourceText[], fileName: string, newProgramText: string) {
168        const file = find(files, f => f.name === fileName)!;
169        file.text = file.text.updateProgram(newProgramText);
170    }
171
172    function checkResolvedTypeDirective(actual: ResolvedTypeReferenceDirective, expected: ResolvedTypeReferenceDirective) {
173        assert.equal(actual.resolvedFileName, expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`);
174        assert.equal(actual.primary, expected.primary, `'primary': expected '${actual.primary}' to be equal to '${expected.primary}'`);
175        return true;
176    }
177
178    function checkCache<T>(caption: string, program: Program, fileName: string, expectedContent: ESMap<string, T> | undefined, getCache: (f: SourceFile) => ESMap<string, T> | undefined, entryChecker: (expected: T, original: T) => boolean): void {
179        const file = program.getSourceFile(fileName);
180        assert.isTrue(file !== undefined, `cannot find file ${fileName}`);
181        const cache = getCache(file!);
182        if (expectedContent === undefined) {
183            assert.isTrue(cache === undefined, `expected ${caption} to be undefined`);
184        }
185        else {
186            assert.isTrue(cache !== undefined, `expected ${caption} to be set`);
187            assert.isTrue(mapsAreEqual(expectedContent, cache!, entryChecker), `contents of ${caption} did not match the expected contents.`);
188        }
189    }
190
191    /** True if the maps have the same keys and values. */
192    function mapsAreEqual<T>(left: ESMap<string, T>, right: ESMap<string, T>, valuesAreEqual?: (left: T, right: T) => boolean): boolean {
193        if (left === right) return true;
194        if (!left || !right) return false;
195        const someInLeftHasNoMatch = forEachEntry(left, (leftValue, leftKey) => {
196            if (!right.has(leftKey)) return true;
197            const rightValue = right.get(leftKey)!;
198            return !(valuesAreEqual ? valuesAreEqual(leftValue, rightValue) : leftValue === rightValue);
199        });
200        if (someInLeftHasNoMatch) return false;
201        const someInRightHasNoMatch = forEachKey(right, rightKey => !left.has(rightKey));
202        return !someInRightHasNoMatch;
203    }
204
205    function checkResolvedModulesCache(program: Program, fileName: string, expectedContent: ESMap<string, ResolvedModule | undefined> | undefined): void {
206        checkCache("resolved modules", program, fileName, expectedContent, f => f.resolvedModules, checkResolvedModule);
207    }
208
209    function checkResolvedTypeDirectivesCache(program: Program, fileName: string, expectedContent: ESMap<string, ResolvedTypeReferenceDirective> | undefined): void {
210        checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective);
211    }
212
213    describe("unittests:: Reuse program structure:: General", () => {
214        const target = ScriptTarget.Latest;
215        const files: NamedSourceText[] = [
216            {
217                name: "a.ts", text: SourceText.New(
218                    `
219/// <reference path='b.ts'/>
220/// <reference path='non-existing-file.ts'/>
221/// <reference types="typerefs" />
222`, "", `var x = 1`)
223            },
224            { name: "b.ts", text: SourceText.New(`/// <reference path='c.ts'/>`, "", `var y = 2`) },
225            { name: "c.ts", text: SourceText.New("", "", `var z = 1;`) },
226            { name: "types/typerefs/index.d.ts", text: SourceText.New("", "", `declare let z: number;`) },
227        ];
228
229        it("successful if change does not affect imports", () => {
230            const program1 = newProgram(files, ["a.ts"], { target });
231            const program2 = updateProgram(program1, ["a.ts"], { target }, files => {
232                files[0].text = files[0].text.updateProgram("var x = 100");
233            });
234            assert.equal(program2.structureIsReused, StructureIsReused.Completely);
235            const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts"));
236            const program2Diagnostics = program2.getSemanticDiagnostics(program1.getSourceFile("a.ts"));
237            assert.equal(program1Diagnostics.length, program2Diagnostics.length);
238        });
239
240        it("successful if change does not affect type reference directives", () => {
241            const program1 = newProgram(files, ["a.ts"], { target });
242            const program2 = updateProgram(program1, ["a.ts"], { target }, files => {
243                files[0].text = files[0].text.updateProgram("var x = 100");
244            });
245            assert.equal(program2.structureIsReused, StructureIsReused.Completely);
246            const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts"));
247            const program2Diagnostics = program2.getSemanticDiagnostics(program1.getSourceFile("a.ts"));
248            assert.equal(program1Diagnostics.length, program2Diagnostics.length);
249        });
250
251        it("fails if change affects tripleslash references", () => {
252            const program1 = newProgram(files, ["a.ts"], { target });
253            const program2 = updateProgram(program1, ["a.ts"], { target }, files => {
254                const newReferences = `/// <reference path='b.ts'/>
255                /// <reference path='c.ts'/>
256                `;
257                files[0].text = files[0].text.updateReferences(newReferences);
258            });
259            assert.equal(program2.structureIsReused, StructureIsReused.SafeModules);
260        });
261
262        it("fails if change affects type references", () => {
263            const program1 = newProgram(files, ["a.ts"], { types: ["a"] });
264            const program2 = updateProgram(program1, ["a.ts"], { types: ["b"] }, noop);
265            assert.equal(program2.structureIsReused, StructureIsReused.Not);
266        });
267
268        it("succeeds if change doesn't affect type references", () => {
269            const program1 = newProgram(files, ["a.ts"], { types: ["a"] });
270            const program2 = updateProgram(program1, ["a.ts"], { types: ["a"] }, noop);
271            assert.equal(program2.structureIsReused, StructureIsReused.Completely);
272        });
273
274        it("fails if change affects imports", () => {
275            const program1 = newProgram(files, ["a.ts"], { target });
276            const program2 = updateProgram(program1, ["a.ts"], { target }, files => {
277                files[2].text = files[2].text.updateImportsAndExports("import x from 'b'");
278            });
279            assert.equal(program2.structureIsReused, StructureIsReused.SafeModules);
280        });
281
282        it("fails if change affects type directives", () => {
283            const program1 = newProgram(files, ["a.ts"], { target });
284            const program2 = updateProgram(program1, ["a.ts"], { target }, files => {
285                const newReferences = `
286/// <reference path='b.ts'/>
287/// <reference path='non-existing-file.ts'/>
288/// <reference types="typerefs1" />`;
289                files[0].text = files[0].text.updateReferences(newReferences);
290            });
291            assert.equal(program2.structureIsReused, StructureIsReused.SafeModules);
292        });
293
294        it("fails if module kind changes", () => {
295            const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS });
296            const program2 = updateProgram(program1, ["a.ts"], { target, module: ModuleKind.AMD }, noop);
297            assert.equal(program2.structureIsReused, StructureIsReused.Not);
298        });
299
300        it("succeeds if rootdir changes", () => {
301            const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/b" });
302            const program2 = updateProgram(program1, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/c" }, noop);
303            assert.equal(program2.structureIsReused, StructureIsReused.Completely);
304        });
305
306        it("fails if config path changes", () => {
307            const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/b/tsconfig.json" });
308            const program2 = updateProgram(program1, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/c/tsconfig.json" }, noop);
309            assert.equal(program2.structureIsReused, StructureIsReused.Not);
310        });
311
312        it("succeeds if missing files remain missing", () => {
313            const options: CompilerOptions = { target, noLib: true };
314
315            const program1 = newProgram(files, ["a.ts"], options);
316            assert.notDeepEqual(emptyArray, program1.getMissingFilePaths());
317
318            const program2 = updateProgram(program1, ["a.ts"], options, noop);
319            assert.deepEqual(program1.getMissingFilePaths(), program2.getMissingFilePaths());
320
321            assert.equal(program2.structureIsReused, StructureIsReused.Completely,);
322        });
323
324        it("fails if missing file is created", () => {
325            const options: CompilerOptions = { target, noLib: true };
326
327            const program1 = newProgram(files, ["a.ts"], options);
328            assert.notDeepEqual(emptyArray, program1.getMissingFilePaths());
329
330            const newTexts: NamedSourceText[] = files.concat([{ name: "non-existing-file.ts", text: SourceText.New("", "", `var x = 1`) }]);
331            const program2 = updateProgram(program1, ["a.ts"], options, noop, newTexts);
332            assert.lengthOf(program2.getMissingFilePaths(), 0);
333
334            assert.equal(program2.structureIsReused, StructureIsReused.Not);
335        });
336
337        it("resolution cache follows imports", () => {
338            (<any>Error).stackTraceLimit = Infinity;
339
340            const files = [
341                { name: "a.ts", text: SourceText.New("", "import {_} from 'b'", "var x = 1") },
342                { name: "b.ts", text: SourceText.New("", "", "var y = 2") },
343            ];
344            const options: CompilerOptions = { target };
345
346            const program1 = newProgram(files, ["a.ts"], options);
347            checkResolvedModulesCache(program1, "a.ts", new Map(getEntries({ b: createResolvedModule("b.ts") })));
348            checkResolvedModulesCache(program1, "b.ts", /*expectedContent*/ undefined);
349
350            const program2 = updateProgram(program1, ["a.ts"], options, files => {
351                files[0].text = files[0].text.updateProgram("var x = 2");
352            });
353            assert.equal(program2.structureIsReused, StructureIsReused.Completely);
354
355            // content of resolution cache should not change
356            checkResolvedModulesCache(program1, "a.ts", new Map(getEntries({ b: createResolvedModule("b.ts") })));
357            checkResolvedModulesCache(program1, "b.ts", /*expectedContent*/ undefined);
358
359            // imports has changed - program is not reused
360            const program3 = updateProgram(program2, ["a.ts"], options, files => {
361                files[0].text = files[0].text.updateImportsAndExports("");
362            });
363            assert.equal(program3.structureIsReused, StructureIsReused.SafeModules);
364            checkResolvedModulesCache(program3, "a.ts", /*expectedContent*/ undefined);
365
366            const program4 = updateProgram(program3, ["a.ts"], options, files => {
367                const newImports = `import x from 'b'
368                import y from 'c'
369                `;
370                files[0].text = files[0].text.updateImportsAndExports(newImports);
371            });
372            assert.equal(program4.structureIsReused, StructureIsReused.SafeModules);
373            checkResolvedModulesCache(program4, "a.ts", new Map(getEntries({ b: createResolvedModule("b.ts"), c: undefined })));
374        });
375
376        it("set the resolvedImports after re-using an ambient external module declaration", () => {
377            const files = [
378                { name: "/a.ts", text: SourceText.New("", "", 'import * as a from "a";') },
379                { name: "/types/zzz/index.d.ts", text: SourceText.New("", "", 'declare module "a" { }') },
380            ];
381            const options: CompilerOptions = { target, typeRoots: ["/types"] };
382            const program1 = newProgram(files, ["/a.ts"], options);
383            const program2 = updateProgram(program1, ["/a.ts"], options, files => {
384                files[0].text = files[0].text.updateProgram('import * as aa from "a";');
385            });
386            assert.isDefined(program2.getSourceFile("/a.ts")!.resolvedModules!.get("a"), "'a' is not an unresolved module after re-use");
387        });
388
389        it("works with updated SourceFiles", () => {
390            // adapted repro from https://github.com/Microsoft/TypeScript/issues/26166
391            const files = [
392                { name: "/a.ts", text: SourceText.New("", "", 'import * as a from "a";a;') },
393                { name: "/types/zzz/index.d.ts", text: SourceText.New("", "", 'declare module "a" { }') },
394            ];
395            const host = createTestCompilerHost(files, target);
396            const options: CompilerOptions = { target, typeRoots: ["/types"] };
397            const program1 = createProgram(["/a.ts"], options, host);
398            let sourceFile = program1.getSourceFile("/a.ts")!;
399            assert.isDefined(sourceFile, "'/a.ts' is included in the program");
400            sourceFile = updateSourceFile(sourceFile, "'use strict';" + sourceFile.text, { newLength: "'use strict';".length, span: { start: 0, length: 0 } });
401            assert.strictEqual(sourceFile.statements[2].getSourceFile(), sourceFile, "parent pointers are updated");
402            const updateHost: TestCompilerHost = {
403                ...host,
404                getSourceFile(fileName) {
405                    return fileName === sourceFile.fileName ? sourceFile : program1.getSourceFile(fileName);
406                }
407            };
408            const program2 = createProgram(["/a.ts"], options, updateHost, program1);
409            assert.isDefined(program2.getSourceFile("/a.ts")!.resolvedModules!.get("a"), "'a' is not an unresolved module after re-use");
410            assert.strictEqual(sourceFile.statements[2].getSourceFile(), sourceFile, "parent pointers are not altered");
411        });
412
413        it("resolved type directives cache follows type directives", () => {
414            const files = [
415                { name: "/a.ts", text: SourceText.New("/// <reference types='typedefs'/>", "", "var x = $") },
416                { name: "/types/typedefs/index.d.ts", text: SourceText.New("", "", "declare var $: number") },
417            ];
418            const options: CompilerOptions = { target, typeRoots: ["/types"] };
419
420            const program1 = newProgram(files, ["/a.ts"], options);
421            checkResolvedTypeDirectivesCache(program1, "/a.ts", new Map(getEntries({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })));
422            checkResolvedTypeDirectivesCache(program1, "/types/typedefs/index.d.ts", /*expectedContent*/ undefined);
423
424            const program2 = updateProgram(program1, ["/a.ts"], options, files => {
425                files[0].text = files[0].text.updateProgram("var x = 2");
426            });
427            assert.equal(program2.structureIsReused, StructureIsReused.Completely);
428
429            // content of resolution cache should not change
430            checkResolvedTypeDirectivesCache(program1, "/a.ts", new Map(getEntries({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })));
431            checkResolvedTypeDirectivesCache(program1, "/types/typedefs/index.d.ts", /*expectedContent*/ undefined);
432
433            // type reference directives has changed - program is not reused
434            const program3 = updateProgram(program2, ["/a.ts"], options, files => {
435                files[0].text = files[0].text.updateReferences("");
436            });
437
438            assert.equal(program3.structureIsReused, StructureIsReused.SafeModules);
439            checkResolvedTypeDirectivesCache(program3, "/a.ts", /*expectedContent*/ undefined);
440
441            const program4 = updateProgram(program3, ["/a.ts"], options, files => {
442                const newReferences = `/// <reference types="typedefs"/>
443                /// <reference types="typedefs2"/>
444                `;
445                files[0].text = files[0].text.updateReferences(newReferences);
446            });
447            assert.equal(program4.structureIsReused, StructureIsReused.SafeModules);
448            checkResolvedTypeDirectivesCache(program1, "/a.ts", new Map(getEntries({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })));
449        });
450
451        it("fetches imports after npm install", () => {
452            const file1Ts = { name: "file1.ts", text: SourceText.New("", `import * as a from "a";`, "const myX: number = a.x;") };
453            const file2Ts = { name: "file2.ts", text: SourceText.New("", "", "") };
454            const indexDTS = { name: "node_modules/a/index.d.ts", text: SourceText.New("", "export declare let x: number;", "") };
455            const options: CompilerOptions = { target: ScriptTarget.ES2015, traceResolution: true, moduleResolution: ModuleResolutionKind.NodeJs };
456            const rootFiles = [file1Ts, file2Ts];
457            const filesAfterNpmInstall = [file1Ts, file2Ts, indexDTS];
458
459            const initialProgram = newProgram(rootFiles, rootFiles.map(f => f.name), options);
460            {
461                assert.deepEqual(initialProgram.host.getTrace(),
462                    [
463                        "======== Resolving module 'a' from 'file1.ts'. ========",
464                        "Explicitly specified module resolution kind: 'NodeJs'.",
465                        "Loading module 'a' from 'node_modules' folder, target file type 'TypeScript'.",
466                        "File 'node_modules/a/package.json' does not exist.",
467                        "File 'node_modules/a.ts' does not exist.",
468                        "File 'node_modules/a.tsx' does not exist.",
469                        "File 'node_modules/a.d.ts' does not exist.",
470                        "File 'node_modules/a.ets' does not exist.",
471                        "File 'node_modules/a/index.ts' does not exist.",
472                        "File 'node_modules/a/index.tsx' does not exist.",
473                        "File 'node_modules/a/index.d.ts' does not exist.",
474                        "File 'node_modules/a/index.ets' does not exist.",
475                        "File 'node_modules/@types/a/package.json' does not exist.",
476                        "File 'node_modules/@types/a.d.ts' does not exist.",
477                        "File 'node_modules/@types/a/index.d.ts' does not exist.",
478                        "Loading module 'a' from 'node_modules' folder, target file type 'JavaScript'.",
479                        "File 'node_modules/a/package.json' does not exist.",
480                        "File 'node_modules/a.js' does not exist.",
481                        "File 'node_modules/a.jsx' does not exist.",
482                        "File 'node_modules/a/index.js' does not exist.",
483                        "File 'node_modules/a/index.jsx' does not exist.",
484                        "======== Module name 'a' was not resolved. ========"
485                    ],
486                    "initialProgram: execute module resolution normally.");
487
488                const initialProgramDiagnostics = initialProgram.getSemanticDiagnostics(initialProgram.getSourceFile("file1.ts"));
489                assert.lengthOf(initialProgramDiagnostics, 1, `initialProgram: import should fail.`);
490            }
491
492            const afterNpmInstallProgram = updateProgram(initialProgram, rootFiles.map(f => f.name), options, f => {
493                f[1].text = f[1].text.updateReferences(`/// <reference no-default-lib="true"/>`);
494            }, filesAfterNpmInstall);
495            {
496                assert.deepEqual(afterNpmInstallProgram.host.getTrace(),
497                    [
498                        "======== Resolving module 'a' from 'file1.ts'. ========",
499                        "Explicitly specified module resolution kind: 'NodeJs'.",
500                        "Loading module 'a' from 'node_modules' folder, target file type 'TypeScript'.",
501                        "File 'node_modules/a/package.json' does not exist.",
502                        "File 'node_modules/a.ts' does not exist.",
503                        "File 'node_modules/a.tsx' does not exist.",
504                        "File 'node_modules/a.d.ts' does not exist.",
505                        "File 'node_modules/a.ets' does not exist.",
506                        "File 'node_modules/a/index.ts' does not exist.",
507                        "File 'node_modules/a/index.tsx' does not exist.",
508                        "File 'node_modules/a/index.d.ts' exist - use it as a name resolution result.",
509                        "======== Module name 'a' was successfully resolved to 'node_modules/a/index.d.ts'. ========"
510                    ],
511                    "afterNpmInstallProgram: execute module resolution normally.");
512
513                const afterNpmInstallProgramDiagnostics = afterNpmInstallProgram.getSemanticDiagnostics(afterNpmInstallProgram.getSourceFile("file1.ts"));
514                assert.lengthOf(afterNpmInstallProgramDiagnostics, 0, `afterNpmInstallProgram: program is well-formed with import.`);
515            }
516        });
517
518        it("can reuse ambient module declarations from non-modified files", () => {
519            const files = [
520                { name: "/a/b/app.ts", text: SourceText.New("", "import * as fs from 'fs'", "") },
521                { name: "/a/b/node.d.ts", text: SourceText.New("", "", "declare module 'fs' {}") }
522            ];
523            const options = { target: ScriptTarget.ES2015, traceResolution: true };
524            const program = newProgram(files, files.map(f => f.name), options);
525            assert.deepEqual(program.host.getTrace(),
526                [
527                    "======== Resolving module 'fs' from '/a/b/app.ts'. ========",
528                    "Module resolution kind is not specified, using 'Classic'.",
529                    "File '/a/b/fs.ts' does not exist.",
530                    "File '/a/b/fs.tsx' does not exist.",
531                    "File '/a/b/fs.d.ts' does not exist.",
532                    "File '/a/b/fs.ets' does not exist.",
533                    "File '/a/fs.ts' does not exist.",
534                    "File '/a/fs.tsx' does not exist.",
535                    "File '/a/fs.d.ts' does not exist.",
536                    "File '/a/fs.ets' does not exist.",
537                    "File '/fs.ts' does not exist.",
538                    "File '/fs.tsx' does not exist.",
539                    "File '/fs.d.ts' does not exist.",
540                    "File '/fs.ets' does not exist.",
541                    "File '/a/b/node_modules/@types/fs/package.json' does not exist.",
542                    "File '/a/b/node_modules/@types/fs.d.ts' does not exist.",
543                    "File '/a/b/node_modules/@types/fs/index.d.ts' does not exist.",
544                    "File '/a/node_modules/@types/fs/package.json' does not exist.",
545                    "File '/a/node_modules/@types/fs.d.ts' does not exist.",
546                    "File '/a/node_modules/@types/fs/index.d.ts' does not exist.",
547                    "File '/node_modules/@types/fs/package.json' does not exist.",
548                    "File '/node_modules/@types/fs.d.ts' does not exist.",
549                    "File '/node_modules/@types/fs/index.d.ts' does not exist.",
550                    "File '/a/b/fs.js' does not exist.",
551                    "File '/a/b/fs.jsx' does not exist.",
552                    "File '/a/fs.js' does not exist.",
553                    "File '/a/fs.jsx' does not exist.",
554                    "File '/fs.js' does not exist.",
555                    "File '/fs.jsx' does not exist.",
556                    "======== Module name 'fs' was not resolved. ========",
557                ], "should look for 'fs'");
558
559            const program2 = updateProgram(program, program.getRootFileNames(), options, f => {
560                f[0].text = f[0].text.updateProgram("var x = 1;");
561            });
562            assert.deepEqual(program2.host.getTrace(), [
563                "Module 'fs' was resolved as ambient module declared in '/a/b/node.d.ts' since this file was not modified."
564            ], "should reuse 'fs' since node.d.ts was not changed");
565
566            const program3 = updateProgram(program2, program2.getRootFileNames(), options, f => {
567                f[0].text = f[0].text.updateProgram("var y = 1;");
568                f[1].text = f[1].text.updateProgram("declare var process: any");
569            });
570            assert.deepEqual(program3.host.getTrace(),
571                [
572                    "======== Resolving module 'fs' from '/a/b/app.ts'. ========",
573                    "Module resolution kind is not specified, using 'Classic'.",
574                    "File '/a/b/fs.ts' does not exist.",
575                    "File '/a/b/fs.tsx' does not exist.",
576                    "File '/a/b/fs.d.ts' does not exist.",
577                    "File '/a/b/fs.ets' does not exist.",
578                    "File '/a/fs.ts' does not exist.",
579                    "File '/a/fs.tsx' does not exist.",
580                    "File '/a/fs.d.ts' does not exist.",
581                    "File '/a/fs.ets' does not exist.",
582                    "File '/fs.ts' does not exist.",
583                    "File '/fs.tsx' does not exist.",
584                    "File '/fs.d.ts' does not exist.",
585                    "File '/fs.ets' does not exist.",
586                    "File '/a/b/node_modules/@types/fs/package.json' does not exist.",
587                    "File '/a/b/node_modules/@types/fs.d.ts' does not exist.",
588                    "File '/a/b/node_modules/@types/fs/index.d.ts' does not exist.",
589                    "File '/a/node_modules/@types/fs/package.json' does not exist.",
590                    "File '/a/node_modules/@types/fs.d.ts' does not exist.",
591                    "File '/a/node_modules/@types/fs/index.d.ts' does not exist.",
592                    "File '/node_modules/@types/fs/package.json' does not exist.",
593                    "File '/node_modules/@types/fs.d.ts' does not exist.",
594                    "File '/node_modules/@types/fs/index.d.ts' does not exist.",
595                    "File '/a/b/fs.js' does not exist.",
596                    "File '/a/b/fs.jsx' does not exist.",
597                    "File '/a/fs.js' does not exist.",
598                    "File '/a/fs.jsx' does not exist.",
599                    "File '/fs.js' does not exist.",
600                    "File '/fs.jsx' does not exist.",
601                    "======== Module name 'fs' was not resolved. ========",
602                ], "should look for 'fs' again since node.d.ts was changed");
603        });
604
605        it("can reuse module resolutions from non-modified files", () => {
606            const files = [
607                { name: "a1.ts", text: SourceText.New("", "", "let x = 1;") },
608                { name: "a2.ts", text: SourceText.New("", "", "let x = 1;") },
609                { name: "b1.ts", text: SourceText.New("", "export class B { x: number; }", "") },
610                { name: "b2.ts", text: SourceText.New("", "export class B { x: number; }", "") },
611                { name: "node_modules/@types/typerefs1/index.d.ts", text: SourceText.New("", "", "declare let z: string;") },
612                { name: "node_modules/@types/typerefs2/index.d.ts", text: SourceText.New("", "", "declare let z: string;") },
613                {
614                    name: "f1.ts",
615                    text:
616                    SourceText.New(
617                        `/// <reference path="a1.ts"/>${newLine}/// <reference types="typerefs1"/>${newLine}/// <reference no-default-lib="true"/>`,
618                        `import { B } from './b1';${newLine}export let BB = B;`,
619                        "declare module './b1' { interface B { y: string; } }")
620                },
621                {
622                    name: "f2.ts",
623                    text: SourceText.New(
624                        `/// <reference path="a2.ts"/>${newLine}/// <reference types="typerefs2"/>`,
625                        `import { B } from './b2';${newLine}import { BB } from './f1';`,
626                        "(new BB).x; (new BB).y;")
627                },
628            ];
629
630            const options: CompilerOptions = { target: ScriptTarget.ES2015, traceResolution: true, moduleResolution: ModuleResolutionKind.Classic };
631            const program1 = newProgram(files, files.map(f => f.name), options);
632            let expectedErrors = 0;
633            {
634                assert.deepEqual(program1.host.getTrace(),
635                    [
636                        "======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========",
637                        "Resolving with primary search path 'node_modules/@types'.",
638                        "File 'node_modules/@types/typerefs1/package.json' does not exist.",
639                        "File 'node_modules/@types/typerefs1/index.d.ts' exist - use it as a name resolution result.",
640                        "======== Type reference directive 'typerefs1' was successfully resolved to 'node_modules/@types/typerefs1/index.d.ts', primary: true. ========",
641                        "======== Resolving module './b1' from 'f1.ts'. ========",
642                        "Explicitly specified module resolution kind: 'Classic'.",
643                        "File 'b1.ts' exist - use it as a name resolution result.",
644                        "======== Module name './b1' was successfully resolved to 'b1.ts'. ========",
645                        "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========",
646                        "Resolving with primary search path 'node_modules/@types'.",
647                        "File 'node_modules/@types/typerefs2/package.json' does not exist.",
648                        "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.",
649                        "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========",
650                        "======== Resolving module './b2' from 'f2.ts'. ========",
651                        "Explicitly specified module resolution kind: 'Classic'.",
652                        "File 'b2.ts' exist - use it as a name resolution result.",
653                        "======== Module name './b2' was successfully resolved to 'b2.ts'. ========",
654                        "======== Resolving module './f1' from 'f2.ts'. ========",
655                        "Explicitly specified module resolution kind: 'Classic'.",
656                        "File 'f1.ts' exist - use it as a name resolution result.",
657                        "======== Module name './f1' was successfully resolved to 'f1.ts'. ========"
658                    ],
659                    "program1: execute module resolution normally.");
660
661                const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("f2.ts"));
662                assert.lengthOf(program1Diagnostics, expectedErrors, `initial program should be well-formed`);
663            }
664            const indexOfF1 = 6;
665            const program2 = updateProgram(program1, program1.getRootFileNames(), options, f => {
666                const newSourceText = f[indexOfF1].text.updateReferences(`/// <reference path="a1.ts"/>${newLine}/// <reference types="typerefs1"/>`);
667                f[indexOfF1] = { name: "f1.ts", text: newSourceText };
668            });
669
670            {
671                const program2Diagnostics = program2.getSemanticDiagnostics(program2.getSourceFile("f2.ts"));
672                assert.lengthOf(program2Diagnostics, expectedErrors, `removing no-default-lib shouldn't affect any types used.`);
673
674                assert.deepEqual(program2.host.getTrace(), [
675                    "======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========",
676                    "Resolving with primary search path 'node_modules/@types'.",
677                    "File 'node_modules/@types/typerefs1/package.json' does not exist.",
678                    "File 'node_modules/@types/typerefs1/index.d.ts' exist - use it as a name resolution result.",
679                    "======== Type reference directive 'typerefs1' was successfully resolved to 'node_modules/@types/typerefs1/index.d.ts', primary: true. ========",
680                    "======== Resolving module './b1' from 'f1.ts'. ========",
681                    "Explicitly specified module resolution kind: 'Classic'.",
682                    "File 'b1.ts' exist - use it as a name resolution result.",
683                    "======== Module name './b1' was successfully resolved to 'b1.ts'. ========",
684                    "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========",
685                    "Resolving with primary search path 'node_modules/@types'.",
686                    "File 'node_modules/@types/typerefs2/package.json' does not exist.",
687                    "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.",
688                    "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========",
689                    "Reusing resolution of module './b2' to file 'f2.ts' from old program.",
690                    "Reusing resolution of module './f1' to file 'f2.ts' from old program."
691                ], "program2: reuse module resolutions in f2 since it is unchanged");
692            }
693
694            const program3 = updateProgram(program2, program2.getRootFileNames(), options, f => {
695                const newSourceText = f[indexOfF1].text.updateReferences(`/// <reference path="a1.ts"/>`);
696                f[indexOfF1] = { name: "f1.ts", text: newSourceText };
697            });
698
699            {
700                const program3Diagnostics = program3.getSemanticDiagnostics(program3.getSourceFile("f2.ts"));
701                assert.lengthOf(program3Diagnostics, expectedErrors, `typerefs2 was unused, so diagnostics should be unaffected.`);
702
703                assert.deepEqual(program3.host.getTrace(), [
704                    "======== Resolving module './b1' from 'f1.ts'. ========",
705                    "Explicitly specified module resolution kind: 'Classic'.",
706                    "File 'b1.ts' exist - use it as a name resolution result.",
707                    "======== Module name './b1' was successfully resolved to 'b1.ts'. ========",
708                    "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========",
709                    "Resolving with primary search path 'node_modules/@types'.",
710                    "File 'node_modules/@types/typerefs2/package.json' does not exist.",
711                    "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.",
712                    "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========",
713                    "Reusing resolution of module './b2' to file 'f2.ts' from old program.",
714                    "Reusing resolution of module './f1' to file 'f2.ts' from old program."
715                ], "program3: reuse module resolutions in f2 since it is unchanged");
716            }
717
718
719            const program4 = updateProgram(program3, program3.getRootFileNames(), options, f => {
720                const newSourceText = f[indexOfF1].text.updateReferences("");
721                f[indexOfF1] = { name: "f1.ts", text: newSourceText };
722            });
723
724            {
725                const program4Diagnostics = program4.getSemanticDiagnostics(program4.getSourceFile("f2.ts"));
726                assert.lengthOf(program4Diagnostics, expectedErrors, `a1.ts was unused, so diagnostics should be unaffected.`);
727
728                assert.deepEqual(program4.host.getTrace(), [
729                    "======== Resolving module './b1' from 'f1.ts'. ========",
730                    "Explicitly specified module resolution kind: 'Classic'.",
731                    "File 'b1.ts' exist - use it as a name resolution result.",
732                    "======== Module name './b1' was successfully resolved to 'b1.ts'. ========",
733                    "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========",
734                    "Resolving with primary search path 'node_modules/@types'.",
735                    "File 'node_modules/@types/typerefs2/package.json' does not exist.",
736                    "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.",
737                    "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========",
738                    "Reusing resolution of module './b2' to file 'f2.ts' from old program.",
739                    "Reusing resolution of module './f1' to file 'f2.ts' from old program."
740                ], "program_4: reuse module resolutions in f2 since it is unchanged");
741            }
742
743            const program5 = updateProgram(program4, program4.getRootFileNames(), options, f => {
744                const newSourceText = f[indexOfF1].text.updateImportsAndExports(`import { B } from './b1';`);
745                f[indexOfF1] = { name: "f1.ts", text: newSourceText };
746            });
747
748            {
749                const program5Diagnostics = program5.getSemanticDiagnostics(program5.getSourceFile("f2.ts"));
750                assert.lengthOf(program5Diagnostics, ++expectedErrors, `import of BB in f1 fails. BB is of type any. Add one error`);
751
752                assert.deepEqual(program5.host.getTrace(), [
753                    "======== Resolving module './b1' from 'f1.ts'. ========",
754                    "Explicitly specified module resolution kind: 'Classic'.",
755                    "File 'b1.ts' exist - use it as a name resolution result.",
756                    "======== Module name './b1' was successfully resolved to 'b1.ts'. ========"
757                ], "program_5: exports do not affect program structure, so f2's resolutions are silently reused.");
758            }
759
760            const program6 = updateProgram(program5, program5.getRootFileNames(), options, f => {
761                const newSourceText = f[indexOfF1].text.updateProgram("");
762                f[indexOfF1] = { name: "f1.ts", text: newSourceText };
763            });
764
765            {
766                const program6Diagnostics = program6.getSemanticDiagnostics(program6.getSourceFile("f2.ts"));
767                assert.lengthOf(program6Diagnostics, expectedErrors, `import of BB in f1 fails.`);
768
769                assert.deepEqual(program6.host.getTrace(), [
770                    "======== Resolving module './b1' from 'f1.ts'. ========",
771                    "Explicitly specified module resolution kind: 'Classic'.",
772                    "File 'b1.ts' exist - use it as a name resolution result.",
773                    "======== Module name './b1' was successfully resolved to 'b1.ts'. ========",
774                    "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========",
775                    "Resolving with primary search path 'node_modules/@types'.",
776                    "File 'node_modules/@types/typerefs2/package.json' does not exist.",
777                    "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.",
778                    "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========",
779                    "Reusing resolution of module './b2' to file 'f2.ts' from old program.",
780                    "Reusing resolution of module './f1' to file 'f2.ts' from old program."
781                ], "program_6: reuse module resolutions in f2 since it is unchanged");
782            }
783
784            const program7 = updateProgram(program6, program6.getRootFileNames(), options, f => {
785                const newSourceText = f[indexOfF1].text.updateImportsAndExports("");
786                f[indexOfF1] = { name: "f1.ts", text: newSourceText };
787            });
788
789            {
790                const program7Diagnostics = program7.getSemanticDiagnostics(program7.getSourceFile("f2.ts"));
791                assert.lengthOf(program7Diagnostics, expectedErrors, `removing import is noop with respect to program, so no change in diagnostics.`);
792
793                assert.deepEqual(program7.host.getTrace(), [
794                    "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========",
795                    "Resolving with primary search path 'node_modules/@types'.",
796                    "File 'node_modules/@types/typerefs2/package.json' does not exist.",
797                    "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.",
798                    "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========",
799                    "Reusing resolution of module './b2' to file 'f2.ts' from old program.",
800                    "Reusing resolution of module './f1' to file 'f2.ts' from old program."
801                ], "program_7 should reuse module resolutions in f2 since it is unchanged");
802            }
803        });
804
805        describe("redirects", () => {
806            const axIndex = "/node_modules/a/node_modules/x/index.d.ts";
807            const axPackage = "/node_modules/a/node_modules/x/package.json";
808            const bxIndex = "/node_modules/b/node_modules/x/index.d.ts";
809            const bxPackage = "/node_modules/b/node_modules/x/package.json";
810            const root = "/a.ts";
811            const compilerOptions = { target, moduleResolution: ModuleResolutionKind.NodeJs };
812
813            function createRedirectProgram(useGetSourceFileByPath: boolean, options?: { bText: string, bVersion: string }): ProgramWithSourceTexts {
814                const files: NamedSourceText[] = [
815                    {
816                        name: "/node_modules/a/index.d.ts",
817                        text: SourceText.New("", 'import X from "x";', "export function a(x: X): void;"),
818                    },
819                    {
820                        name: axIndex,
821                        text: SourceText.New("", "", "export default class X { private x: number; }"),
822                    },
823                    {
824                        name: axPackage,
825                        text: SourceText.New("", "", JSON.stringify({ name: "x", version: "1.2.3" })),
826                    },
827                    {
828                        name: "/node_modules/b/index.d.ts",
829                        text: SourceText.New("", 'import X from "x";', "export const b: X;"),
830                    },
831                    {
832                        name: bxIndex,
833                        text: SourceText.New("", "", options ? options.bText : "export default class X { private x: number; }"),
834                    },
835                    {
836                        name: bxPackage,
837                        text: SourceText.New("", "", JSON.stringify({ name: "x", version: options ? options.bVersion : "1.2.3" })),
838                    },
839                    {
840                        name: root,
841                        text: SourceText.New("", 'import { a } from "a"; import { b } from "b";', "a(b)"),
842                    },
843                ];
844
845                return newProgram(files, [root], compilerOptions, useGetSourceFileByPath);
846            }
847
848            function updateRedirectProgram(program: ProgramWithSourceTexts, updater: (files: NamedSourceText[]) => void, useGetSourceFileByPath: boolean): ProgramWithSourceTexts {
849                return updateProgram(program, [root], compilerOptions, updater, /*newTexts*/ undefined, useGetSourceFileByPath);
850            }
851
852            function verifyRedirects(useGetSourceFileByPath: boolean) {
853                it("No changes -> redirect not broken", () => {
854                    const program1 = createRedirectProgram(useGetSourceFileByPath);
855
856                    const program2 = updateRedirectProgram(program1, files => {
857                        updateProgramText(files, root, "const x = 1;");
858                    }, useGetSourceFileByPath);
859                    assert.equal(program2.structureIsReused, StructureIsReused.Completely);
860                    assert.lengthOf(program2.getSemanticDiagnostics(), 0);
861                });
862
863                it("Target changes -> redirect broken", () => {
864                    const program1 = createRedirectProgram(useGetSourceFileByPath);
865                    assert.lengthOf(program1.getSemanticDiagnostics(), 0);
866
867                    const program2 = updateRedirectProgram(program1, files => {
868                        updateProgramText(files, axIndex, "export default class X { private x: number; private y: number; }");
869                        updateProgramText(files, axPackage, JSON.stringify('{ name: "x", version: "1.2.4" }'));
870                    }, useGetSourceFileByPath);
871                    assert.equal(program2.structureIsReused, StructureIsReused.Not);
872                    assert.lengthOf(program2.getSemanticDiagnostics(), 1);
873                });
874
875                it("Underlying changes -> redirect broken", () => {
876                    const program1 = createRedirectProgram(useGetSourceFileByPath);
877
878                    const program2 = updateRedirectProgram(program1, files => {
879                        updateProgramText(files, bxIndex, "export default class X { private x: number; private y: number; }");
880                        updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.4" }));
881                    }, useGetSourceFileByPath);
882                    assert.equal(program2.structureIsReused, StructureIsReused.Not);
883                    assert.lengthOf(program2.getSemanticDiagnostics(), 1);
884                });
885
886                it("Previously duplicate packages -> program structure not reused", () => {
887                    const program1 = createRedirectProgram(useGetSourceFileByPath, { bVersion: "1.2.4", bText: "export = class X { private x: number; }" });
888
889                    const program2 = updateRedirectProgram(program1, files => {
890                        updateProgramText(files, bxIndex, "export default class X { private x: number; }");
891                        updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.3" }));
892                    }, useGetSourceFileByPath);
893                    assert.equal(program2.structureIsReused, StructureIsReused.Not);
894                    assert.deepEqual(program2.getSemanticDiagnostics(), []);
895                });
896            }
897
898            describe("when host implements getSourceFile", () => {
899                verifyRedirects(/*useGetSourceFileByPath*/ false);
900            });
901            describe("when host implements getSourceFileByPath", () => {
902                verifyRedirects(/*useGetSourceFileByPath*/ true);
903            });
904        });
905    });
906
907    describe("unittests:: Reuse program structure:: host is optional", () => {
908        it("should work if host is not provided", () => {
909            createProgram([], {});
910        });
911    });
912
913    type File = TestFSWithWatch.File;
914    import createTestSystem = TestFSWithWatch.createWatchedSystem;
915    import libFile = TestFSWithWatch.libFile;
916
917    describe("unittests:: Reuse program structure:: isProgramUptoDate", () => {
918        function getWhetherProgramIsUptoDate(
919            program: Program,
920            newRootFileNames: string[],
921            newOptions: CompilerOptions
922        ) {
923            return isProgramUptoDate(
924                program, newRootFileNames, newOptions,
925                path => program.getSourceFileByPath(path)!.version, /*fileExists*/ returnFalse,
926                /*hasInvalidatedResolution*/ returnFalse,
927                /*hasChangedAutomaticTypeDirectiveNames*/ undefined,
928                /*projectReferences*/ undefined
929            );
930        }
931
932        function duplicate(options: CompilerOptions): CompilerOptions;
933        function duplicate(fileNames: string[]): string[];
934        function duplicate(filesOrOptions: CompilerOptions | string[]) {
935            return JSON.parse(JSON.stringify(filesOrOptions));
936        }
937
938        describe("should return true when there is no change in compiler options and", () => {
939            function verifyProgramIsUptoDate(
940                program: Program,
941                newRootFileNames: string[],
942                newOptions: CompilerOptions
943            ) {
944                const actual = getWhetherProgramIsUptoDate(program, newRootFileNames, newOptions);
945                assert.isTrue(actual);
946            }
947
948            function verifyProgramWithoutConfigFile(system: System, rootFiles: string[], options: CompilerOptions) {
949                const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions({
950                    rootFiles,
951                    options,
952                    watchOptions: undefined,
953                    system
954                })).getCurrentProgram().getProgram();
955                verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options));
956            }
957
958            function verifyProgramWithConfigFile(system: System, configFileName: string) {
959                const program = createWatchProgram(createWatchCompilerHostOfConfigFile({
960                    configFileName,
961                    system
962                })).getCurrentProgram().getProgram();
963                const { fileNames, options } = parseConfigFileWithSystem(configFileName, {}, /*watchOptionsToExtend*/ undefined, system, notImplemented)!; // TODO: GH#18217
964                verifyProgramIsUptoDate(program, fileNames, options);
965            }
966
967            function verifyProgram(files: File[], rootFiles: string[], options: CompilerOptions, configFile: string) {
968                const system = createTestSystem(files);
969                verifyProgramWithoutConfigFile(system, rootFiles, options);
970                verifyProgramWithConfigFile(system, configFile);
971            }
972
973            it("has empty options", () => {
974                const file1: File = {
975                    path: "/a/b/file1.ts",
976                    content: "let x = 1"
977                };
978                const file2: File = {
979                    path: "/a/b/file2.ts",
980                    content: "let y = 1"
981                };
982                const configFile: File = {
983                    path: "/a/b/tsconfig.json",
984                    content: "{}"
985                };
986                verifyProgram([file1, file2, libFile, configFile], [file1.path, file2.path], {}, configFile.path);
987            });
988
989            it("has lib specified in the options", () => {
990                const compilerOptions: CompilerOptions = { lib: ["es5", "es2015.promise"] };
991                const app: File = {
992                    path: "/src/app.ts",
993                    content: "var x: Promise<string>;"
994                };
995                const configFile: File = {
996                    path: "/src/tsconfig.json",
997                    content: JSON.stringify({ compilerOptions })
998                };
999                const es5Lib: File = {
1000                    path: "/compiler/lib.es5.d.ts",
1001                    content: "declare const eval: any"
1002                };
1003                const es2015Promise: File = {
1004                    path: "/compiler/lib.es2015.promise.d.ts",
1005                    content: "declare class Promise<T> {}"
1006                };
1007
1008                verifyProgram([app, configFile, es5Lib, es2015Promise], [app.path], compilerOptions, configFile.path);
1009            });
1010
1011            it("has paths specified in the options", () => {
1012                const compilerOptions: CompilerOptions = {
1013                    baseUrl: ".",
1014                    paths: {
1015                        "*": [
1016                            "packages/mail/data/*",
1017                            "packages/styles/*",
1018                            "*"
1019                        ]
1020                    }
1021                };
1022                const app: File = {
1023                    path: "/src/packages/framework/app.ts",
1024                    content: 'import classc from "module1/lib/file1";\
1025                              import classD from "module3/file3";\
1026                              let x = new classc();\
1027                              let y = new classD();'
1028                };
1029                const module1: File = {
1030                    path: "/src/packages/mail/data/module1/lib/file1.ts",
1031                    content: 'import classc from "module2/file2";export default classc;',
1032                };
1033                const module2: File = {
1034                    path: "/src/packages/mail/data/module1/lib/module2/file2.ts",
1035                    content: 'class classc { method2() { return "hello"; } }\nexport default classc',
1036                };
1037                const module3: File = {
1038                    path: "/src/packages/styles/module3/file3.ts",
1039                    content: "class classD { method() { return 10; } }\nexport default classD;"
1040                };
1041                const configFile: File = {
1042                    path: "/src/tsconfig.json",
1043                    content: JSON.stringify({ compilerOptions })
1044                };
1045
1046                verifyProgram([app, module1, module2, module3, libFile, configFile], [app.path], compilerOptions, configFile.path);
1047            });
1048
1049            it("has include paths specified in tsconfig file", () => {
1050                const compilerOptions: CompilerOptions = {
1051                    baseUrl: ".",
1052                    paths: {
1053                        "*": [
1054                            "packages/mail/data/*",
1055                            "packages/styles/*",
1056                            "*"
1057                        ]
1058                    }
1059                };
1060                const app: File = {
1061                    path: "/src/packages/framework/app.ts",
1062                    content: 'import classc from "module1/lib/file1";\
1063                              import classD from "module3/file3";\
1064                              let x = new classc();\
1065                              let y = new classD();'
1066                };
1067                const module1: File = {
1068                    path: "/src/packages/mail/data/module1/lib/file1.ts",
1069                    content: 'import classc from "module2/file2";export default classc;',
1070                };
1071                const module2: File = {
1072                    path: "/src/packages/mail/data/module1/lib/module2/file2.ts",
1073                    content: 'class classc { method2() { return "hello"; } }\nexport default classc',
1074                };
1075                const module3: File = {
1076                    path: "/src/packages/styles/module3/file3.ts",
1077                    content: "class classD { method() { return 10; } }\nexport default classD;"
1078                };
1079                const configFile: File = {
1080                    path: "/src/tsconfig.json",
1081                    content: JSON.stringify({ compilerOptions, include: ["packages/**/*.ts"] })
1082                };
1083                verifyProgramWithConfigFile(createTestSystem([app, module1, module2, module3, libFile, configFile]), configFile.path);
1084            });
1085            it("has the same root file names", () => {
1086                const module1: File = {
1087                    path: "/src/packages/mail/data/module1/lib/file1.ts",
1088                    content: 'import classc from "module2/file2";export default classc;',
1089                };
1090                const module2: File = {
1091                    path: "/src/packages/mail/data/module1/lib/module2/file2.ts",
1092                    content: 'class classc { method2() { return "hello"; } }\nexport default classc',
1093                };
1094                const module3: File = {
1095                    path: "/src/packages/styles/module3/file3.ts",
1096                    content: "class classD { method() { return 10; } }\nexport default classD;"
1097                };
1098                const rootFiles = [module1.path, module2.path, module3.path];
1099                const system = createTestSystem([module1, module2, module3]);
1100                const options = {};
1101                const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions({
1102                    rootFiles,
1103                    options,
1104                    watchOptions: undefined,
1105                    system
1106                })).getCurrentProgram().getProgram();
1107                verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options));
1108            });
1109
1110        });
1111        describe("should return false when there is no change in compiler options but", () => {
1112            function verifyProgramIsNotUptoDate(
1113                program: Program,
1114                newRootFileNames: string[],
1115                newOptions: CompilerOptions
1116            ) {
1117                const actual = getWhetherProgramIsUptoDate(program, newRootFileNames, newOptions);
1118                assert.isFalse(actual);
1119            }
1120            it("has more root file names", () => {
1121                const module1: File = {
1122                    path: "/src/packages/mail/data/module1/lib/file1.ts",
1123                    content: 'import classc from "module2/file2";export default classc;',
1124                };
1125                const module2: File = {
1126                    path: "/src/packages/mail/data/module1/lib/module2/file2.ts",
1127                    content: 'class classc { method2() { return "hello"; } }\nexport default classc',
1128                };
1129                const module3: File = {
1130                    path: "/src/packages/styles/module3/file3.ts",
1131                    content: "class classD { method() { return 10; } }\nexport default classD;"
1132                };
1133                const rootFiles = [module1.path, module2.path];
1134                const newRootFiles = [module1.path, module2.path, module3.path];
1135                const system = createTestSystem([module1, module2, module3]);
1136                const options = {};
1137                const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions({
1138                    rootFiles,
1139                    options,
1140                    watchOptions: undefined,
1141                    system
1142                })).getCurrentProgram().getProgram();
1143                verifyProgramIsNotUptoDate(program, duplicate(newRootFiles), duplicate(options));
1144            });
1145            it("has one root file replaced by another", () => {
1146                const module1: File = {
1147                    path: "/src/packages/mail/data/module1/lib/file1.ts",
1148                    content: 'import classc from "module2/file2";export default classc;',
1149                };
1150                const module2: File = {
1151                    path: "/src/packages/mail/data/module1/lib/module2/file2.ts",
1152                    content: 'class classc { method2() { return "hello"; } }\nexport default classc',
1153                };
1154                const module3: File = {
1155                    path: "/src/packages/styles/module3/file3.ts",
1156                    content: "class classD { method() { return 10; } }\nexport default classD;"
1157                };
1158                const rootFiles = [module1.path, module2.path];
1159                const newRootFiles = [module2.path, module3.path];
1160                const system = createTestSystem([module1, module2, module3]);
1161                const options = {};
1162                const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions({
1163                    rootFiles,
1164                    options,
1165                    watchOptions: undefined,
1166                    system
1167                })).getCurrentProgram().getProgram();
1168                verifyProgramIsNotUptoDate(program, duplicate(newRootFiles), duplicate(options));
1169            });
1170        });
1171    });
1172}
1173