/// /// /// namespace ts { const enum ChangedPart { references = 1 << 0, importsAndExports = 1 << 1, program = 1 << 2 } const newLine = "\r\n"; interface SourceFileWithText extends SourceFile { sourceText?: SourceText; } interface NamedSourceText { name: string; text: SourceText; } interface ProgramWithSourceTexts extends Program { sourceTexts?: NamedSourceText[]; } class SourceText implements IScriptSnapshot { private fullText: string; constructor(private references: string, private importsAndExports: string, private program: string, private changedPart: ChangedPart = 0, private version = 0) { } static New(references: string, importsAndExports: string, program: string): SourceText { Debug.assert(references !== undefined); Debug.assert(importsAndExports !== undefined); Debug.assert(program !== undefined); return new SourceText(references + newLine, importsAndExports + newLine, program || ""); } public getVersion(): number { return this.version; } public updateReferences(newReferences: string): SourceText { Debug.assert(newReferences !== undefined); return new SourceText(newReferences + newLine, this.importsAndExports, this.program, this.changedPart | ChangedPart.references, this.version + 1); } public updateImportsAndExports(newImportsAndExports: string): SourceText { Debug.assert(newImportsAndExports !== undefined); return new SourceText(this.references, newImportsAndExports + newLine, this.program, this.changedPart | ChangedPart.importsAndExports, this.version + 1); } public updateProgram(newProgram: string): SourceText { Debug.assert(newProgram !== undefined); return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1); } public getFullText() { return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program); } public getText(start: number, end: number): string { return this.getFullText().substring(start, end); } getLength(): number { return this.getFullText().length; } getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange { const oldText = oldSnapshot; let oldSpan: TextSpan; let newLength: number; switch (oldText.changedPart ^ this.changedPart) { case ChangedPart.references: oldSpan = createTextSpan(0, oldText.references.length); newLength = this.references.length; break; case ChangedPart.importsAndExports: oldSpan = createTextSpan(oldText.references.length, oldText.importsAndExports.length); newLength = this.importsAndExports.length; break; case ChangedPart.program: oldSpan = createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length); newLength = this.program.length; break; default: Debug.assert(false, "Unexpected change"); } return createTextChangeRange(oldSpan, newLength); } } function createTestCompilerHost(texts: NamedSourceText[], target: ScriptTarget): CompilerHost { const files: Map = {}; for (const t of texts) { const file = createSourceFile(t.name, t.text.getFullText(), target); file.sourceText = t.text; files[t.name] = file; } return { getSourceFile(fileName): SourceFile { return files[fileName]; }, getDefaultLibFileName(): string { return "lib.d.ts"; }, writeFile(file, text) { throw new Error("NYI"); }, getCurrentDirectory(): string { return ""; }, getDirectories(path: string): string[] { return []; }, getCanonicalFileName(fileName): string { return sys && sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); }, useCaseSensitiveFileNames(): boolean { return sys && sys.useCaseSensitiveFileNames; }, getNewLine(): string { return sys ? sys.newLine : newLine; }, fileExists: fileName => hasProperty(files, fileName), readFile: fileName => { const file = lookUp(files, fileName); return file && file.text; } }; } function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions): Program { const host = createTestCompilerHost(texts, options.target); const program = createProgram(rootNames, options, host); program.sourceTexts = texts; return program; } function updateProgram(oldProgram: Program, rootNames: string[], options: CompilerOptions, updater: (files: NamedSourceText[]) => void) { const texts: NamedSourceText[] = (oldProgram).sourceTexts.slice(0); updater(texts); const host = createTestCompilerHost(texts, options.target); const program = createProgram(rootNames, options, host, oldProgram); program.sourceTexts = texts; return program; } function getSizeOfMap(map: Map): number { let size = 0; for (const id in map) { if (hasProperty(map, id)) { size++; } } return size; } function checkResolvedModule(expected: ResolvedModule, actual: ResolvedModule): void { assert.isTrue(actual !== undefined); assert.isTrue(expected.resolvedFileName === actual.resolvedFileName, `'resolvedFileName': expected '${expected.resolvedFileName}' to be equal to '${actual.resolvedFileName}'`); assert.isTrue(expected.isExternalLibraryImport === actual.isExternalLibraryImport, `'isExternalLibraryImport': expected '${expected.isExternalLibraryImport}' to be equal to '${actual.isExternalLibraryImport}'`); } function checkResolvedTypeDirective(expected: ResolvedTypeReferenceDirective, actual: ResolvedTypeReferenceDirective): void { assert.isTrue(actual !== undefined); assert.isTrue(expected.resolvedFileName === actual.resolvedFileName, `'resolvedFileName': expected '${expected.resolvedFileName}' to be equal to '${actual.resolvedFileName}'`); assert.isTrue(expected.primary === actual.primary, `'primary': expected '${expected.primary}' to be equal to '${actual.primary}'`); } function checkCache(caption: string, program: Program, fileName: string, expectedContent: Map, getCache: (f: SourceFile) => Map, entryChecker: (expected: T, original: T) => void): void { const file = program.getSourceFile(fileName); assert.isTrue(file !== undefined, `cannot find file ${fileName}`); const cache = getCache(file); if (expectedContent === undefined) { assert.isTrue(cache === undefined, `expected ${caption} to be undefined`); } else { assert.isTrue(cache !== undefined, `expected ${caption} to be set`); const actualCacheSize = getSizeOfMap(cache); const expectedSize = getSizeOfMap(expectedContent); assert.isTrue(actualCacheSize === expectedSize, `expected actual size: ${actualCacheSize} to be equal to ${expectedSize}`); for (const id in expectedContent) { if (hasProperty(expectedContent, id)) { if (expectedContent[id]) { const expected = expectedContent[id]; const actual = cache[id]; entryChecker(expected, actual); } } else { assert.isTrue(cache[id] === undefined); } } } } function checkResolvedModulesCache(program: Program, fileName: string, expectedContent: Map): void { checkCache("resolved modules", program, fileName, expectedContent, f => f.resolvedModules, checkResolvedModule); } function checkResolvedTypeDirectivesCache(program: Program, fileName: string, expectedContent: Map): void { checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); } describe("Reuse program structure", () => { const target = ScriptTarget.Latest; const files = [ { name: "a.ts", text: SourceText.New( ` /// /// /// `, "", `var x = 1`) }, { name: "b.ts", text: SourceText.New(`/// `, "", `var y = 2`) }, { name: "c.ts", text: SourceText.New("", "", `var z = 1;`) }, { name: "types/typerefs/index.d.ts", text: SourceText.New("", "", `declare let z: number;`) }, ]; it("successful if change does not affect imports", () => { const program_1 = newProgram(files, ["a.ts"], { target }); const program_2 = updateProgram(program_1, ["a.ts"], { target }, files => { files[0].text = files[0].text.updateProgram("var x = 100"); }); assert.isTrue(program_1.structureIsReused); const program1Diagnostics = program_1.getSemanticDiagnostics(program_1.getSourceFile("a.ts")); const program2Diagnostics = program_2.getSemanticDiagnostics(program_1.getSourceFile("a.ts")); assert.equal(program1Diagnostics.length, program2Diagnostics.length); }); it("successful if change does not affect type reference directives", () => { const program_1 = newProgram(files, ["a.ts"], { target }); const program_2 = updateProgram(program_1, ["a.ts"], { target }, files => { files[0].text = files[0].text.updateProgram("var x = 100"); }); assert.isTrue(program_1.structureIsReused); const program1Diagnostics = program_1.getSemanticDiagnostics(program_1.getSourceFile("a.ts")); const program2Diagnostics = program_2.getSemanticDiagnostics(program_1.getSourceFile("a.ts")); assert.equal(program1Diagnostics.length, program2Diagnostics.length); }); it("fails if change affects tripleslash references", () => { const program_1 = newProgram(files, ["a.ts"], { target }); updateProgram(program_1, ["a.ts"], { target }, files => { const newReferences = `/// /// `; files[0].text = files[0].text.updateReferences(newReferences); }); assert.isTrue(!program_1.structureIsReused); }); it("fails if change affects imports", () => { const program_1 = newProgram(files, ["a.ts"], { target }); updateProgram(program_1, ["a.ts"], { target }, files => { files[2].text = files[2].text.updateImportsAndExports("import x from 'b'"); }); assert.isTrue(!program_1.structureIsReused); }); it("fails if change affects type directives", () => { const program_1 = newProgram(files, ["a.ts"], { target }); updateProgram(program_1, ["a.ts"], { target }, files => { const newReferences = ` /// /// /// `; files[0].text = files[0].text.updateReferences(newReferences); }); assert.isTrue(!program_1.structureIsReused); }); it("fails if module kind changes", () => { const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS }); updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.AMD }, files => void 0); assert.isTrue(!program_1.structureIsReused); }); it("fails if rootdir changes", () => { const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/b" }); updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/c" }, files => void 0); assert.isTrue(!program_1.structureIsReused); }); it("fails if config path changes", () => { const program_1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/b/tsconfig.json" }); updateProgram(program_1, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/c/tsconfig.json" }, files => void 0); assert.isTrue(!program_1.structureIsReused); }); it("resolution cache follows imports", () => { const files = [ { name: "a.ts", text: SourceText.New("", "import {_} from 'b'", "var x = 1") }, { name: "b.ts", text: SourceText.New("", "", "var y = 2") }, ]; const options: CompilerOptions = { target }; const program_1 = newProgram(files, ["a.ts"], options); checkResolvedModulesCache(program_1, "a.ts", { "b": { resolvedFileName: "b.ts" } }); checkResolvedModulesCache(program_1, "b.ts", undefined); const program_2 = updateProgram(program_1, ["a.ts"], options, files => { files[0].text = files[0].text.updateProgram("var x = 2"); }); assert.isTrue(program_1.structureIsReused); // content of resolution cache should not change checkResolvedModulesCache(program_1, "a.ts", { "b": { resolvedFileName: "b.ts" } }); checkResolvedModulesCache(program_1, "b.ts", undefined); // imports has changed - program is not reused const program_3 = updateProgram(program_2, ["a.ts"], options, files => { files[0].text = files[0].text.updateImportsAndExports(""); }); assert.isTrue(!program_2.structureIsReused); checkResolvedModulesCache(program_3, "a.ts", undefined); const program_4 = updateProgram(program_3, ["a.ts"], options, files => { const newImports = `import x from 'b' import y from 'c' `; files[0].text = files[0].text.updateImportsAndExports(newImports); }); assert.isTrue(!program_3.structureIsReused); checkResolvedModulesCache(program_4, "a.ts", { "b": { resolvedFileName: "b.ts" }, "c": undefined }); }); it("resolved type directives cache follows type directives", () => { const files = [ { name: "/a.ts", text: SourceText.New("/// ", "", "var x = $") }, { name: "/types/typedefs/index.d.ts", text: SourceText.New("", "", "declare var $: number") }, ]; const options: CompilerOptions = { target, typeRoots: ["/types"] }; const program_1 = newProgram(files, ["/a.ts"], options); checkResolvedTypeDirectivesCache(program_1, "/a.ts", { "typedefs": { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } }); checkResolvedTypeDirectivesCache(program_1, "/types/typedefs/index.d.ts", undefined); const program_2 = updateProgram(program_1, ["/a.ts"], options, files => { files[0].text = files[0].text.updateProgram("var x = 2"); }); assert.isTrue(program_1.structureIsReused); // content of resolution cache should not change checkResolvedTypeDirectivesCache(program_1, "/a.ts", { "typedefs": { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } }); checkResolvedTypeDirectivesCache(program_1, "/types/typedefs/index.d.ts", undefined); // type reference directives has changed - program is not reused const program_3 = updateProgram(program_2, ["/a.ts"], options, files => { files[0].text = files[0].text.updateReferences(""); }); assert.isTrue(!program_2.structureIsReused); checkResolvedTypeDirectivesCache(program_3, "/a.ts", undefined); updateProgram(program_3, ["/a.ts"], options, files => { const newReferences = `/// /// `; files[0].text = files[0].text.updateReferences(newReferences); }); assert.isTrue(!program_3.structureIsReused); checkResolvedTypeDirectivesCache(program_1, "/a.ts", { "typedefs": { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } }); }); }); describe("host is optional", () => { it("should work if host is not provided", () => { createProgram([], {}); }); }); }