diff --git a/Jakefile.js b/Jakefile.js index 07a2ade06a6..f2e61c4579d 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -138,7 +138,8 @@ var harnessSources = [ "services/patternMatcher.ts", "versionCache.ts", "convertToBase64.ts", - "transpile.ts" + "transpile.ts", + "reuseProgramStructure.ts" ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index a4878569329..024cc96cb79 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5634,8 +5634,6 @@ namespace ts { // will immediately bail out of walking any subtrees when we can see that their parents // are already correct. let result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /* setParentNode */ true) - // pass set of modules that were resolved before so 'createProgram' can reuse previous resolution results - result.resolvedModules = sourceFile.resolvedModules; return result; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index f187b68ef5b..3f19c28a9b2 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -163,9 +163,13 @@ namespace ts { let filesByName = createFileMap(fileName => host.getCanonicalFileName(fileName)); - let structureIsReused = oldProgram && host.hasChanges && tryReuseStructureFromOldProgram(); - - if (!structureIsReused) { + // if old program was provided by has different target module kind - assume that it cannot be reused + // different module kind can lead to different way of resolving modules + if (oldProgram && oldProgram.getCompilerOptions().module !== options.module) { + oldProgram = undefined; + } + + if (!tryReuseStructureFromOldProgram()) { forEach(rootNames, name => processRootFile(name, false)); // Do not process the default library if: // - The '--noLib' flag is used. @@ -218,9 +222,15 @@ namespace ts { } function tryReuseStructureFromOldProgram(): boolean { + if (!host.hasChanges) { + // host does not support method 'hasChanges' + return false; + } if (!oldProgram) { return false; } + + Debug.assert(!oldProgram.structureIsReused); // there is an old program, check if we can reuse its structure let oldRootNames = oldProgram.getRootFileNames(); @@ -258,6 +268,8 @@ namespace ts { // imports has changed return false; } + // pass the cache of module resolutions from the old source file + newSourceFile.resolvedModules = oldSourceFile.resolvedModules; } else { // file has no changes - use it as is @@ -275,6 +287,8 @@ namespace ts { files = newSourceFiles; + oldProgram.structureIsReused = true; + return true; } @@ -576,14 +590,26 @@ namespace ts { function processImportedModules(file: SourceFile, basePath: string) { collectExternalModuleReferences(file); - if (file.imports.length) { - let allImportsInTheCache = true; - // check that all imports are contained in resolved modules cache - // if at least one of imports in not in the cache - cache needs to be reinitialized - for (let moduleName of file.imports) { - if (!hasResolvedModuleName(file, moduleName)) { - allImportsInTheCache = false; - break; + if (file.imports.length) { + let allImportsInTheCache = false; + + // try to grab existing module resolutions from the old source file + let oldSourceFile: SourceFile = oldProgram && oldProgram.getSourceFile(file.fileName); + if (oldSourceFile) { + file.resolvedModules = oldSourceFile.resolvedModules; + + // check that all imports are contained in resolved modules cache + // if at least one of imports in not in the cache - cache needs to be reinitialized + checkImports: { + if (file.resolvedModules) { + for (let moduleName of file.imports) { + if (!hasResolvedModuleName(file, moduleName)) { + break checkImports; + } + } + + allImportsInTheCache = true; + } } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index baf55c1537d..7c7b0c52eef 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1244,6 +1244,7 @@ namespace ts { /* @internal */ getIdentifierCount(): number; /* @internal */ getSymbolCount(): number; /* @internal */ getTypeCount(): number; + /* @internal */ structureIsReused?: boolean; } export interface SourceMapSpan { diff --git a/tests/cases/unittests/reuseProgramStructure.ts b/tests/cases/unittests/reuseProgramStructure.ts new file mode 100644 index 00000000000..996f6bb316b --- /dev/null +++ b/tests/cases/unittests/reuseProgramStructure.ts @@ -0,0 +1,261 @@ +/// +/// +/// + +module ts { + + const enum ChangedPart { + references = 1 << 0, + importsAndExports = 1 << 1, + program = 1 << 2 + } + + let 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 { + var oldText = oldSnapshot; + var oldSpan: TextSpan; + var 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 { + let files: Map = {}; + for (let t of texts) { + let 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 ""; + }, + getCanonicalFileName(fileName): string { + return sys.useCaseSensitiveFileNames ? fileName: fileName.toLowerCase(); + }, + useCaseSensitiveFileNames(): boolean { + return sys.useCaseSensitiveFileNames; + }, + getNewLine() : string { + return sys.newLine; + }, + hasChanges(oldFile: SourceFileWithText): boolean { + let current = files[oldFile.fileName]; + return !current || oldFile.sourceText.getVersion() !== current.sourceText.getVersion(); + } + + } + } + + function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions): Program { + var host = createTestCompilerHost(texts, options.target); + let program = createProgram(rootNames, options, host); + program.sourceTexts = texts; + return program; + } + + function updateProgram(oldProgram: Program, rootNames: string[], options: CompilerOptions, updater: (files: NamedSourceText[]) => void) { + var texts: NamedSourceText[] = (oldProgram).sourceTexts.slice(0); + updater(texts); + var host = createTestCompilerHost(texts, options.target); + var program = createProgram(rootNames, options, host, oldProgram); + program.sourceTexts = texts; + return program; + } + + function getSizeOfMap(map: Map): number { + let size = 0; + for (let id in map) { + if (hasProperty(map, id)) { + size++; + } + } + return size; + } + + function checkResolvedModulesCache(program: Program, fileName: string, expectedContent: Map): void { + let file = program.getSourceFile(fileName); + assert.isTrue(file !==undefined, `cannot find file ${fileName}`); + if (expectedContent === undefined) { + assert.isTrue(file.resolvedModules === undefined, "expected resolvedModules to be undefined"); + } + else { + assert.isTrue(file.resolvedModules !== undefined, "expected resolvedModuled to be set"); + let actualCacheSize = getSizeOfMap(file.resolvedModules); + let expectedSize = getSizeOfMap(expectedContent); + assert.isTrue(actualCacheSize === expectedSize, `expected actual size: ${actualCacheSize} to be equal to ${expectedSize}`); + + for (let id in expectedContent) { + if (hasProperty(expectedContent, id)) { + assert.isTrue(hasProperty(file.resolvedModules, id), `expected ${id} to be found in resolved modules`); + assert.isTrue(expectedContent[id] === file.resolvedModules[id], `expected '${expectedContent[id]}' to be equal to '${file.resolvedModules[id]}'`); + } + } + } + } + + describe("Reuse program structure", () => { + let target = ScriptTarget.Latest; + let 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;`)}, + ] + + it("successful if change does not affect imports", () => { + var program_1 = newProgram(files, ["a.ts"], {target}); + var program_2 = updateProgram(program_1, ["a.ts"], {target}, files => { + files[0].text = files[0].text.updateProgram("var x = 100"); + }); + assert.isTrue(program_1.structureIsReused); + }); + + it("fails if change affects tripleslash references", () => { + var program_1 = newProgram(files, ["a.ts"], {target}); + var program_2 = updateProgram(program_1, ["a.ts"], {target}, files => { + let newReferences = `/// + /// + `; + files[0].text = files[0].text.updateReferences(newReferences); + }); + assert.isTrue(!program_1.structureIsReused); + }); + + it("fails if change affects imports", () => { + var program_1 = newProgram(files, ["a.ts"], {target}); + var program_2 = 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 module kind changes", () => { + var program_1 = newProgram(files, ["a.ts"], {target, module: ModuleKind.CommonJS}); + var program_2 = updateProgram(program_1, ["a.ts"], {target, module: ModuleKind.AMD}, files => void 0); + assert.isTrue(!program_1.structureIsReused); + }); + + it("resolution cache follows imports", () => { + let files = [ + { name: "a.ts", text: SourceText.New("", "import {_} from 'b'", "var x = 1") }, + { name: "b.ts", text: SourceText.New("", "", "var y = 2") }, + ]; + var options: CompilerOptions = {target}; + + var program_1 = newProgram(files, ["a.ts"], options); + checkResolvedModulesCache(program_1, "a.ts", { "b": "b.ts" }); + checkResolvedModulesCache(program_1, "b.ts", undefined); + + var 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": "b.ts" }); + checkResolvedModulesCache(program_1, "b.ts", undefined); + + // imports has changed - program is not reused + var 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); + + var program_4 = updateProgram(program_3, ["a.ts"], options, files => { + let 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": "b.ts", "c": undefined}); + }); + }) +} \ No newline at end of file