diff --git a/Jakefile.js b/Jakefile.js index 517f5e862d510..1ddb6fd7f0890 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/checker.ts b/src/compiler/checker.ts index cb26eb371b5ff..e439311b42d3c 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -929,7 +929,9 @@ namespace ts { // Escape the name in the "require(...)" clause to ensure we find the right symbol. let moduleName = escapeIdentifier(moduleReferenceLiteral.text); - if (!moduleName) return; + if (!moduleName) { + return; + } let isRelative = isExternalModuleNameRelative(moduleName); if (!isRelative) { let symbol = getSymbol(globals, '"' + moduleName + '"', SymbolFlags.ValueModule); @@ -937,20 +939,9 @@ namespace ts { return symbol; } } - let fileName: string; - let sourceFile: SourceFile; - while (true) { - fileName = normalizePath(combinePaths(searchPath, moduleName)); - sourceFile = forEach(supportedExtensions, extension => host.getSourceFile(fileName + extension)); - if (sourceFile || isRelative) { - break; - } - let parentPath = getDirectoryPath(searchPath); - if (parentPath === searchPath) { - break; - } - searchPath = parentPath; - } + + let fileName = getResolvedModuleFileName(getSourceFile(location), moduleReferenceLiteral.text); + let sourceFile = fileName && host.getSourceFile(fileName); if (sourceFile) { if (sourceFile.symbol) { return sourceFile.symbol; diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 28efaf24cca19..957b519af5e29 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -9,7 +9,9 @@ namespace ts { /** The version of the TypeScript compiler release */ export const version = "1.5.3"; - + + var emptyArray: any[] = []; + export function findConfigFile(searchPath: string): string { var fileName = "tsconfig.json"; while (true) { @@ -143,7 +145,7 @@ namespace ts { } } - export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost): Program { + export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program { let program: Program; let files: SourceFile[] = []; let diagnostics = createDiagnosticCollection(); @@ -160,22 +162,39 @@ namespace ts { host = host || createCompilerHost(options); let filesByName = createFileMap(fileName => host.getCanonicalFileName(fileName)); - - forEach(rootNames, name => processRootFile(name, /*isDefaultLib:*/ false)); - - // Do not process the default library if: - // - The '--noLib' flag is used. - // - A 'no-default-lib' reference comment is encountered in - // processing the root files. - if (!skipDefaultLib) { - processRootFile(host.getDefaultLibFileName(options), /*isDefaultLib:*/ true); + + if (oldProgram) { + // check properties that can affect structure of the program or module resolution strategy + // if any of these properties has changed - structure cannot be reused + let oldOptions = oldProgram.getCompilerOptions(); + if ((oldOptions.module !== options.module) || + (oldOptions.noResolve !== options.noResolve) || + (oldOptions.target !== options.target) || + (oldOptions.noLib !== options.noLib)) { + oldProgram = undefined; + } + } + + if (!tryReuseStructureFromOldProgram()) { + forEach(rootNames, name => processRootFile(name, false)); + // Do not process the default library if: + // - The '--noLib' flag is used. + // - A 'no-default-lib' reference comment is encountered in + // processing the root files. + if (!skipDefaultLib) { + processRootFile(host.getDefaultLibFileName(options), true); + } } verifyCompilerOptions(); + // unconditionally set oldProgram to undefined to prevent it from being captured in closure + oldProgram = undefined; + programTime += new Date().getTime() - start; program = { + getRootFileNames: () => rootNames, getSourceFile: getSourceFile, getSourceFiles: () => files, getCompilerOptions: () => options, @@ -211,6 +230,70 @@ namespace ts { return classifiableNames; } + function tryReuseStructureFromOldProgram(): boolean { + if (!oldProgram) { + return false; + } + + Debug.assert(!oldProgram.structureIsReused); + + // there is an old program, check if we can reuse its structure + let oldRootNames = oldProgram.getRootFileNames(); + if (!arrayIsEqualTo(oldRootNames, rootNames)) { + return false; + } + + // check if program source files has changed in the way that can affect structure of the program + let newSourceFiles: SourceFile[] = []; + for (let oldSourceFile of oldProgram.getSourceFiles()) { + let newSourceFile = host.getSourceFile(oldSourceFile.fileName, options.target); + if (!newSourceFile) { + return false; + } + + if (oldSourceFile !== newSourceFile) { + if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) { + // value of no-default-lib has changed + // this will affect if default library is injected into the list of files + return false; + } + + // check tripleslash references + if (!arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { + // tripleslash references has changed + return false; + } + + // check imports + collectExternalModuleReferences(newSourceFile); + if (!arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { + // 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 + newSourceFile = oldSourceFile; + } + + // if file has passed all checks it should be safe to reuse it + newSourceFiles.push(newSourceFile); + } + + // update fileName -> file mapping + for (let file of newSourceFiles) { + filesByName.set(file.fileName, file); + } + + files = newSourceFiles; + + oldProgram.structureIsReused = true; + + return true; + } + function getEmitHost(writeFileCallback?: WriteFileCallback): EmitHost { return { getCanonicalFileName: fileName => host.getCanonicalFileName(fileName), @@ -370,6 +453,62 @@ namespace ts { function processRootFile(fileName: string, isDefaultLib: boolean) { processSourceFile(normalizePath(fileName), isDefaultLib); + } + + function fileReferenceIsEqualTo(a: FileReference, b: FileReference): boolean { + return a.fileName === b.fileName; + } + + function moduleNameIsEqualTo(a: LiteralExpression, b: LiteralExpression): boolean { + return a.text === b.text; + } + + function collectExternalModuleReferences(file: SourceFile): void { + if (file.imports) { + return; + } + + let imports: LiteralExpression[]; + for (let node of file.statements) { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ExportDeclaration: + let moduleNameExpr = getExternalModuleName(node); + if (!moduleNameExpr || moduleNameExpr.kind !== SyntaxKind.StringLiteral) { + break; + } + if (!(moduleNameExpr).text) { + break; + } + + (imports || (imports = [])).push(moduleNameExpr); + break; + case SyntaxKind.ModuleDeclaration: + if ((node).name.kind === SyntaxKind.StringLiteral && (node.flags & NodeFlags.Ambient || isDeclarationFile(file))) { + // TypeScript 1.0 spec (April 2014): 12.1.6 + // An AmbientExternalModuleDeclaration declares an external module. + // This type of declaration is permitted only in the global module. + // The StringLiteral must specify a top - level external module name. + // Relative external module names are not permitted + forEachChild((node).body, node => { + if (isExternalModuleImportEqualsDeclaration(node) && + getExternalModuleImportEqualsDeclarationExpression(node).kind === SyntaxKind.StringLiteral) { + let moduleName = getExternalModuleImportEqualsDeclarationExpression(node); + // TypeScript 1.0 spec (April 2014): 12.1.6 + // An ExternalImportDeclaration in anAmbientExternalModuleDeclaration may reference other external modules + // only through top - level external module names. Relative external module names are not permitted. + if (moduleName) { + (imports || (imports = [])).push(moduleName); + } + } + }); + } + break; + } + } + + file.imports = imports || emptyArray; } function processSourceFile(fileName: string, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number) { @@ -487,57 +626,58 @@ namespace ts { processSourceFile(normalizePath(referencedFileName), /* isDefaultLib */ false, file, ref.pos, ref.end); }); } - - function processImportedModules(file: SourceFile, basePath: string) { - forEach(file.statements, node => { - if (node.kind === SyntaxKind.ImportDeclaration || node.kind === SyntaxKind.ImportEqualsDeclaration || node.kind === SyntaxKind.ExportDeclaration) { - let moduleNameExpr = getExternalModuleName(node); - if (moduleNameExpr && moduleNameExpr.kind === SyntaxKind.StringLiteral) { - let moduleNameText = (moduleNameExpr).text; - if (moduleNameText) { - let searchPath = basePath; - let searchName: string; - while (true) { - searchName = normalizePath(combinePaths(searchPath, moduleNameText)); - if (forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, moduleNameExpr))) { - break; - } - let parentPath = getDirectoryPath(searchPath); - if (parentPath === searchPath) { - break; - } - searchPath = parentPath; - } - } - } - } - else if (node.kind === SyntaxKind.ModuleDeclaration && (node).name.kind === SyntaxKind.StringLiteral && (node.flags & NodeFlags.Ambient || isDeclarationFile(file))) { - // TypeScript 1.0 spec (April 2014): 12.1.6 - // An AmbientExternalModuleDeclaration declares an external module. - // This type of declaration is permitted only in the global module. - // The StringLiteral must specify a top - level external module name. - // Relative external module names are not permitted - forEachChild((node).body, node => { - if (isExternalModuleImportEqualsDeclaration(node) && - getExternalModuleImportEqualsDeclarationExpression(node).kind === SyntaxKind.StringLiteral) { - - let nameLiteral = getExternalModuleImportEqualsDeclarationExpression(node); - let moduleName = nameLiteral.text; - if (moduleName) { - // TypeScript 1.0 spec (April 2014): 12.1.6 - // An ExternalImportDeclaration in anAmbientExternalModuleDeclaration may reference other external modules - // only through top - level external module names. Relative external module names are not permitted. - let searchName = normalizePath(combinePaths(basePath, moduleName)); - forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, nameLiteral)); - } - } - }); + + function processImportedModules(file: SourceFile, basePath: string) { + collectExternalModuleReferences(file); + if (file.imports.length) { + file.resolvedModules = {}; + let oldSourceFile = oldProgram && oldProgram.getSourceFile(file.fileName); + for (let moduleName of file.imports) { + resolveModule(moduleName, oldSourceFile && oldSourceFile.resolvedModules); } - }); + + } + else { + // no imports - drop cached module resolutions + file.resolvedModules = undefined; + } + return; function findModuleSourceFile(fileName: string, nameLiteral: Expression) { return findSourceFile(fileName, /* isDefaultLib */ false, file, nameLiteral.pos, nameLiteral.end - nameLiteral.pos); } + + function resolveModule(moduleNameExpr: LiteralExpression, existingResolutions: Map): void { + let searchPath = basePath; + let searchName: string; + + if (existingResolutions && hasProperty(existingResolutions, moduleNameExpr.text)) { + let fileName = existingResolutions[moduleNameExpr.text]; + // use existing resolution + setResolvedModuleName(file, moduleNameExpr.text, fileName); + if (fileName) { + findModuleSourceFile(fileName, moduleNameExpr); + } + return; + } + + while (true) { + searchName = normalizePath(combinePaths(searchPath, moduleNameExpr.text)); + let referencedSourceFile = forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, moduleNameExpr)); + if (referencedSourceFile) { + setResolvedModuleName(file, moduleNameExpr.text, referencedSourceFile.fileName); + return; + } + + let parentPath = getDirectoryPath(searchPath); + if (parentPath === searchPath) { + break; + } + searchPath = parentPath; + } + // mark reference as non-resolved + setResolvedModuleName(file, moduleNameExpr.text, undefined); + } } function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6c2f72a1ab794..31a8fbb408258 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1272,8 +1272,11 @@ namespace ts { // Stores a line map for the file. // This field should never be used directly to obtain line map, use getLineMap function instead. /* @internal */ lineMap: number[]; - /* @internal */ classifiableNames?: Map; + // Stores a mapping 'external module reference text' -> 'resolved file name' | undefined + // Content of this fiels should never be used directly - use getResolvedModuleFileName/setResolvedModuleFileName functions instead + /* @internal */ resolvedModules: Map; + /* @internal */ imports: LiteralExpression[]; } export interface ScriptReferenceHost { @@ -1300,6 +1303,12 @@ namespace ts { } export interface Program extends ScriptReferenceHost { + + /** + * Get a list of root file names that were passed to a 'createProgram' + */ + getRootFileNames(): string[] + /** * Get a list of files in the program */ @@ -1340,6 +1349,9 @@ namespace ts { /* @internal */ getIdentifierCount(): number; /* @internal */ getSymbolCount(): number; /* @internal */ getTypeCount(): number; + + // For testing purposes only. + /* @internal */ structureIsReused?: boolean; } export interface SourceMapSpan { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e66e1925519ed..043ed3efda77a 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -78,6 +78,41 @@ namespace ts { return node.end - node.pos; } + export function arrayIsEqualTo(arr1: T[], arr2: T[], comparer?: (a: T, b: T) => boolean): boolean { + if (!arr1 || !arr2) { + return arr1 === arr2; + } + + if (arr1.length !== arr2.length) { + return false; + } + + for (let i = 0; i < arr1.length; ++i) { + let equals = comparer ? comparer(arr1[i], arr2[i]) : arr1[i] === arr2[i]; + if (!equals) { + return false; + } + } + + return true; + } + + export function hasResolvedModuleName(sourceFile: SourceFile, moduleNameText: string): boolean { + return sourceFile.resolvedModules && hasProperty(sourceFile.resolvedModules, moduleNameText); + } + + export function getResolvedModuleFileName(sourceFile: SourceFile, moduleNameText: string): string { + return hasResolvedModuleName(sourceFile, moduleNameText) ? sourceFile.resolvedModules[moduleNameText] : undefined; + } + + export function setResolvedModuleName(sourceFile: SourceFile, moduleNameText: string, resolvedFileName: string): void { + if (!sourceFile.resolvedModules) { + sourceFile.resolvedModules = {}; + } + + sourceFile.resolvedModules[moduleNameText] = resolvedFileName; + } + // Returns true if this node contains a parse error anywhere underneath it. export function containsParseError(node: Node): boolean { aggregateChildData(node); diff --git a/src/services/services.ts b/src/services/services.ts index 3f656fe75ff82..8972d0b07d0de 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -753,7 +753,8 @@ namespace ts { public languageVariant: LanguageVariant; public identifiers: Map; public nameTable: Map; - + public resolvedModules: Map; + public imports: LiteralExpression[]; private namedDeclarations: Map; public update(newText: string, textChangeRange: TextChangeRange): SourceFile { @@ -1883,7 +1884,7 @@ namespace ts { let getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames); function getKeyFromCompilationSettings(settings: CompilerOptions): string { - return "_" + settings.target; // + "|" + settings.propagateEnumConstantoString() + return "_" + settings.target + "|" + settings.module + "|" + settings.noResolve; } function getBucketForCompilationSettings(settings: CompilerOptions, createIfMissing: boolean): FileMap { @@ -2481,8 +2482,8 @@ namespace ts { getNewLine: () => host.getNewLine ? host.getNewLine() : "\r\n", getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: (fileName, data, writeByteOrderMark) => { }, - getCurrentDirectory: () => host.getCurrentDirectory() - }); + getCurrentDirectory: () => host.getCurrentDirectory(), + }, program); // Release any files we have acquired in the old program but are // not part of the new program. diff --git a/tests/cases/unittests/reuseProgramStructure.ts b/tests/cases/unittests/reuseProgramStructure.ts new file mode 100644 index 0000000000000..1cb389f7717fc --- /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 diff --git a/tests/cases/unittests/services/documentRegistry.ts b/tests/cases/unittests/services/documentRegistry.ts index 8fc466b857f0e..50a144d305653 100644 --- a/tests/cases/unittests/services/documentRegistry.ts +++ b/tests/cases/unittests/services/documentRegistry.ts @@ -30,10 +30,15 @@ describe("DocumentRegistry", () => { assert(f1 !== f3, "Changed target: Expected to have different instances of document"); - compilerOptions.module = ts.ModuleKind.CommonJS; + compilerOptions.preserveConstEnums = true; var f4 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); - assert(f3 === f4, "Changed module: Expected to have the same instance of the document"); + assert(f3 === f4, "Changed preserveConstEnums: Expected to have the same instance of the document"); + + compilerOptions.module = ts.ModuleKind.System; + var f5 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); + + assert(f4 !== f5, "Changed module: Expected to have different instances of the document"); }); it("Acquiring document gets correct version 1", () => {