From 226deec4b0404361778451a7052c27c9f86082a5 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 22 Jun 2015 17:48:44 -0700 Subject: [PATCH 1/9] reuse structure of the program if changes in files don't affect imports/references, remove module resolution from the checker --- src/compiler/checker.ts | 21 +--- src/compiler/parser.ts | 3 +- src/compiler/program.ts | 243 +++++++++++++++++++++++++++++--------- src/compiler/types.ts | 14 ++- src/compiler/utilities.ts | 34 ++++++ src/services/services.ts | 10 +- 6 files changed, 250 insertions(+), 75 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a18f2921a9a53..99bcf9f1617e7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -892,7 +892,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); @@ -900,20 +902,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); + let sourceFile = fileName && host.getSourceFile(fileName); if (sourceFile) { if (sourceFile.symbol) { return sourceFile.symbol; diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index fbb3dad5037b8..a4878569329da 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5634,7 +5634,8 @@ 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 2af9ad9987d4f..878f4eda32283 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,15 +162,18 @@ namespace ts { host = host || createCompilerHost(options); let filesByName = createFileMap(fileName => host.getCanonicalFileName(fileName)); + + let structureIsReused = oldProgram && host.hasChanges && tryReuseStructureFromOldProgram(); - 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 (!structureIsReused) { + 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(); @@ -176,6 +181,7 @@ namespace ts { programTime += new Date().getTime() - start; program = { + getRootFileNames: () => rootNames, getSourceFile: getSourceFile, getSourceFiles: () => files, getCompilerOptions: () => options, @@ -211,6 +217,67 @@ namespace ts { return classifiableNames; } + function tryReuseStructureFromOldProgram(): boolean { + if (!oldProgram) { + return false; + } + + // there is an old program, check if we can reuse its structure + let oldRootNames = oldProgram.getRootFileNames(); + if (rootNames.length !== oldRootNames.length) { + // different amount of root names - structure cannot be reused + return false; + } + + for (let i = 0; i < rootNames.length; i++) { + if (oldRootNames[i] !== rootNames[i]) { + // different order of root names - structure cannot be reused + 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: SourceFile; + if (host.hasChanges(oldSourceFile)) { + newSourceFile = host.getSourceFile(oldSourceFile.fileName, options.target); + if (!newSourceFile) { + 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; + } + } + 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; + + return true; + } + function getEmitHost(writeFileCallback?: WriteFileCallback): EmitHost { return { getCanonicalFileName: fileName => host.getCanonicalFileName(fileName), @@ -332,6 +399,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) { @@ -450,57 +573,69 @@ 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; - } - } + + 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; } } - 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)); - } - } - }); + + if (!allImportsInTheCache) { + // initialize resolvedModules with empty map + // old module resolutions still can be used to avoid actual file system probing + let resolvedModules = file.resolvedModules; + file.resolvedModules = {}; + for (let moduleName of file.imports) { + resolveModule(moduleName, 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]; + 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, referencedSourceFile.fileName); + return; + } + + let parentPath = getDirectoryPath(searchPath); + if (parentPath === searchPath) { + break; + } + searchPath = parentPath; + } + // mark reference as non-resolved + setResolvedModuleName(file, moduleNameExpr, undefined); + } } function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f9a2fa3b7b434..baf55c1537d3a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1176,8 +1176,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 { @@ -1195,6 +1198,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 */ @@ -1881,7 +1890,7 @@ namespace ts { CarriageReturnLineFeed = 0, LineFeed = 1, } - + export interface LineAndCharacter { line: number; /* @@ -2065,6 +2074,7 @@ namespace ts { getCanonicalFileName(fileName: string): string; useCaseSensitiveFileNames(): boolean; getNewLine(): string; + hasChanges?(oldFile: SourceFile): boolean; } export interface TextSpan { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index ee75454ad9331..f6534a1b7e5c3 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -78,6 +78,40 @@ 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) { + if (!comparer(arr1[i], arr2[i])) { + return false; + } + } + + return true; + } + + export function hasResolvedModuleName(sourceFile: SourceFile, moduleName: LiteralExpression): boolean { + return sourceFile.resolvedModules && hasProperty(sourceFile.resolvedModules, moduleName.text); + } + + export function getResolvedModuleFileName(sourceFile: SourceFile, moduleName: LiteralExpression): string { + return sourceFile.resolvedModules && sourceFile.resolvedModules[moduleName.text]; + } + + export function setResolvedModuleName(sourceFile: SourceFile, moduleName: LiteralExpression, resolvedFileName: string): void { + if (!sourceFile.resolvedModules) { + sourceFile.resolvedModules = {}; + } + + sourceFile.resolvedModules[moduleName.text] = 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 70022cfe0bf47..acb844f201105 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -753,7 +753,8 @@ namespace ts { public languageVersion: ScriptTarget; public identifiers: Map; public nameTable: Map; - + public resolvedModules: Map; + public imports: LiteralExpression[]; private namedDeclarations: Map; public update(newText: string, textChangeRange: TextChangeRange): SourceFile { @@ -2471,6 +2472,8 @@ namespace ts { let newSettings = hostCache.compilationSettings(); let changesInCompilationSettingsAffectSyntax = oldSettings && oldSettings.target !== newSettings.target; + let reusableOldProgram = changesInCompilationSettingsAffectSyntax ? undefined : program; + // Now create a new compiler let newProgram = createProgram(hostCache.getRootFileNames(), newSettings, { getSourceFile: getOrCreateSourceFile, @@ -2480,8 +2483,9 @@ namespace ts { getNewLine: () => host.getNewLine ? host.getNewLine() : "\r\n", getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: (fileName, data, writeByteOrderMark) => { }, - getCurrentDirectory: () => host.getCurrentDirectory() - }); + getCurrentDirectory: () => host.getCurrentDirectory(), + hasChanges: oldFile => oldFile.version !== hostCache.getVersion(oldFile.fileName) + }, reusableOldProgram); // Release any files we have acquired in the old program but are // not part of the new program. From 39e832da55f198c0d854cf8c2659cbbb047ac664 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 23 Jun 2015 10:51:00 -0700 Subject: [PATCH 2/9] use existing information about module resolutions --- src/compiler/program.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 878f4eda32283..f187b68ef5b5b 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -613,6 +613,8 @@ namespace ts { if (existingResolutions && hasProperty(existingResolutions, moduleNameExpr.text)) { let fileName = existingResolutions[moduleNameExpr.text]; + // use existing resolution + setResolvedModuleName(file, moduleNameExpr, fileName); if (fileName) { findModuleSourceFile(fileName, moduleNameExpr); } From ba3eb0d0cff37bafc18c52d22d235275f96cb5c2 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 23 Jun 2015 21:06:57 -0700 Subject: [PATCH 3/9] added Program.structureIsReused property, disallow reuse if target module kind differs in old and new programs, move setting of resolvedModules cache to the program, added tests --- Jakefile.js | 3 +- src/compiler/parser.ts | 2 - src/compiler/program.ts | 48 +++- src/compiler/types.ts | 1 + .../cases/unittests/reuseProgramStructure.ts | 261 ++++++++++++++++++ 5 files changed, 301 insertions(+), 14 deletions(-) create mode 100644 tests/cases/unittests/reuseProgramStructure.ts diff --git a/Jakefile.js b/Jakefile.js index 07a2ade06a6a3..f2e61c4579d82 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 a4878569329da..024cc96cb79f3 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 f187b68ef5b5b..3f19c28a9b29e 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 baf55c1537d3a..7c7b0c52eefc9 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 0000000000000..996f6bb316b45 --- /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 From 16deccdf987c15764d8481f73d964b6ecf3b4760 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 24 Jun 2015 13:20:43 -0700 Subject: [PATCH 4/9] revert unintentional change --- src/compiler/parser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 024cc96cb79f3..361c14040bf34 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5634,6 +5634,7 @@ 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) + return result; } From df508de39037b9386fc7bc496e3c191d829530ac Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 24 Jun 2015 15:18:22 -0700 Subject: [PATCH 5/9] fix formatting in parser --- src/compiler/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 361c14040bf34..fbb3dad5037b8 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5634,7 +5634,7 @@ 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) - + return result; } From c968b3653e2cfba46e1491777d2e6917d12c7156 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 24 Jun 2015 17:40:04 -0700 Subject: [PATCH 6/9] addressed PR feedback --- src/compiler/checker.ts | 2 +- src/compiler/program.ts | 49 ++++++++----------- src/compiler/types.ts | 1 - src/compiler/utilities.ts | 17 ++++--- src/services/services.ts | 7 +-- .../unittests/services/documentRegistry.ts | 9 +++- 6 files changed, 39 insertions(+), 46 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b7020344aab5b..fdffb81321620 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -899,7 +899,7 @@ namespace ts { } } - let fileName = getResolvedModuleFileName(getSourceFile(location), moduleReferenceLiteral); + let fileName = getResolvedModuleFileName(getSourceFile(location), moduleReferenceLiteral.text); let sourceFile = fileName && host.getSourceFile(fileName); if (sourceFile) { if (sourceFile.symbol) { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 3f19c28a9b29e..7d1b0ce5fbf5f 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -163,10 +163,14 @@ namespace ts { let filesByName = createFileMap(fileName => host.getCanonicalFileName(fileName)); - // 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 (oldProgram) { + 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()) { @@ -222,10 +226,6 @@ namespace ts { } function tryReuseStructureFromOldProgram(): boolean { - if (!host.hasChanges) { - // host does not support method 'hasChanges' - return false; - } if (!oldProgram) { return false; } @@ -234,28 +234,19 @@ namespace ts { // there is an old program, check if we can reuse its structure let oldRootNames = oldProgram.getRootFileNames(); - if (rootNames.length !== oldRootNames.length) { - // different amount of root names - structure cannot be reused + if (!arrayIsEqualTo(oldRootNames, rootNames)) { return false; } - for (let i = 0; i < rootNames.length; i++) { - if (oldRootNames[i] !== rootNames[i]) { - // different order of root names - structure cannot be reused - 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: SourceFile; - if (host.hasChanges(oldSourceFile)) { - newSourceFile = host.getSourceFile(oldSourceFile.fileName, options.target); - if (!newSourceFile) { - return false; - } - + let newSourceFile = host.getSourceFile(oldSourceFile.fileName, options.target); + if (!newSourceFile) { + return false; + } + + if (oldSourceFile !== newSourceFile) { // check tripleslash references if (!arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { // tripleslash references has changed @@ -420,7 +411,7 @@ namespace ts { } function moduleNameIsEqualTo(a: LiteralExpression, b: LiteralExpression): boolean { - return a.text ===b.text; + return a.text === b.text; } function collectExternalModuleReferences(file: SourceFile): void { @@ -603,7 +594,7 @@ namespace ts { checkImports: { if (file.resolvedModules) { for (let moduleName of file.imports) { - if (!hasResolvedModuleName(file, moduleName)) { + if (!hasResolvedModuleName(file, moduleName.text)) { break checkImports; } } @@ -640,7 +631,7 @@ namespace ts { if (existingResolutions && hasProperty(existingResolutions, moduleNameExpr.text)) { let fileName = existingResolutions[moduleNameExpr.text]; // use existing resolution - setResolvedModuleName(file, moduleNameExpr, fileName); + setResolvedModuleName(file, moduleNameExpr.text, fileName); if (fileName) { findModuleSourceFile(fileName, moduleNameExpr); } @@ -651,7 +642,7 @@ namespace ts { searchName = normalizePath(combinePaths(searchPath, moduleNameExpr.text)); let referencedSourceFile = forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, moduleNameExpr)); if (referencedSourceFile) { - setResolvedModuleName(file, moduleNameExpr, referencedSourceFile.fileName); + setResolvedModuleName(file, moduleNameExpr.text, referencedSourceFile.fileName); return; } @@ -662,7 +653,7 @@ namespace ts { searchPath = parentPath; } // mark reference as non-resolved - setResolvedModuleName(file, moduleNameExpr, undefined); + setResolvedModuleName(file, moduleNameExpr.text, undefined); } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1dbb3cabe09d1..79ac4e85dd850 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2075,7 +2075,6 @@ namespace ts { getCanonicalFileName(fileName: string): string; useCaseSensitiveFileNames(): boolean; getNewLine(): string; - hasChanges?(oldFile: SourceFile): boolean; } export interface TextSpan { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 41cf35e0483e3..150101b641b9c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -78,7 +78,7 @@ namespace ts { return node.end - node.pos; } - export function arrayIsEqualTo(arr1: T[], arr2: T[], comparer: (a: T, b: T) => boolean): boolean { + export function arrayIsEqualTo(arr1: T[], arr2: T[], comparer?: (a: T, b: T) => boolean): boolean { if (!arr1 || !arr2) { return arr1 === arr2; } @@ -88,7 +88,8 @@ namespace ts { } for (let i = 0; i < arr1.length; ++i) { - if (!comparer(arr1[i], arr2[i])) { + let equals = comparer ? comparer(arr1[i], arr2[i]) : arr1[i] === arr2[i]; + if (!equals) { return false; } } @@ -96,20 +97,20 @@ namespace ts { return true; } - export function hasResolvedModuleName(sourceFile: SourceFile, moduleName: LiteralExpression): boolean { - return sourceFile.resolvedModules && hasProperty(sourceFile.resolvedModules, moduleName.text); + export function hasResolvedModuleName(sourceFile: SourceFile, moduleNameText: string): boolean { + return sourceFile.resolvedModules && hasProperty(sourceFile.resolvedModules, moduleNameText); } - export function getResolvedModuleFileName(sourceFile: SourceFile, moduleName: LiteralExpression): string { - return sourceFile.resolvedModules && sourceFile.resolvedModules[moduleName.text]; + export function getResolvedModuleFileName(sourceFile: SourceFile, moduleNameText: string): string { + return hasResolvedModuleName(sourceFile, moduleNameText) ? sourceFile.resolvedModules[moduleNameText]: undefined; } - export function setResolvedModuleName(sourceFile: SourceFile, moduleName: LiteralExpression, resolvedFileName: string): void { + export function setResolvedModuleName(sourceFile: SourceFile, moduleNameText: string, resolvedFileName: string): void { if (!sourceFile.resolvedModules) { sourceFile.resolvedModules = {}; } - sourceFile.resolvedModules[moduleName.text] = resolvedFileName; + sourceFile.resolvedModules[moduleNameText] = resolvedFileName; } // Returns true if this node contains a parse error anywhere underneath it. diff --git a/src/services/services.ts b/src/services/services.ts index a27327b77b46a..b8363fb4598f5 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1898,7 +1898,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 { @@ -2472,8 +2472,6 @@ namespace ts { let newSettings = hostCache.compilationSettings(); let changesInCompilationSettingsAffectSyntax = oldSettings && oldSettings.target !== newSettings.target; - let reusableOldProgram = changesInCompilationSettingsAffectSyntax ? undefined : program; - // Now create a new compiler let newProgram = createProgram(hostCache.getRootFileNames(), newSettings, { getSourceFile: getOrCreateSourceFile, @@ -2484,8 +2482,7 @@ namespace ts { getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: (fileName, data, writeByteOrderMark) => { }, getCurrentDirectory: () => host.getCurrentDirectory(), - hasChanges: oldFile => oldFile.version !== hostCache.getVersion(oldFile.fileName) - }, reusableOldProgram); + }, 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/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", () => { From 66f673618a0a00ec298478ba37a70df751984f62 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 24 Jun 2015 18:12:02 -0700 Subject: [PATCH 7/9] addressed PR feedback --- src/compiler/program.ts | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 7d1b0ce5fbf5f..27154a6610ac1 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -582,37 +582,12 @@ namespace ts { function processImportedModules(file: SourceFile, basePath: string) { collectExternalModuleReferences(file); 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.text)) { - break checkImports; - } - } - - allImportsInTheCache = true; - } - } + file.resolvedModules = {}; + let oldSourceFile = oldProgram && oldProgram.getSourceFile(file.fileName); + for (let moduleName of file.imports) { + resolveModule(moduleName, oldSourceFile && oldSourceFile.resolvedModules); } - if (!allImportsInTheCache) { - // initialize resolvedModules with empty map - // old module resolutions still can be used to avoid actual file system probing - let resolvedModules = file.resolvedModules; - file.resolvedModules = {}; - for (let moduleName of file.imports) { - resolveModule(moduleName, resolvedModules); - } - } } else { // no imports - drop cached module resolutions From 2685d409d577549f12d83420919cc442ecce13c2 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 9 Jul 2015 14:40:33 -0700 Subject: [PATCH 8/9] addressed PR feedback --- src/compiler/program.ts | 10 +- src/compiler/types.ts | 2 + src/compiler/utilities.ts | 2 +- .../cases/unittests/reuseProgramStructure.ts | 510 +++++++++--------- 4 files changed, 267 insertions(+), 257 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 27154a6610ac1..13c49989347b8 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -164,6 +164,8 @@ namespace ts { let filesByName = createFileMap(fileName => host.getCanonicalFileName(fileName)); 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) || @@ -246,7 +248,13 @@ namespace ts { return false; } - if (oldSourceFile !== newSourceFile) { + 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 diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 79ac4e85dd850..0fbbf0053ff8e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1244,6 +1244,8 @@ namespace ts { /* @internal */ getIdentifierCount(): number; /* @internal */ getSymbolCount(): number; /* @internal */ getTypeCount(): number; + + // For testing purposes only. /* @internal */ structureIsReused?: boolean; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 150101b641b9c..bb5112c58082c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -102,7 +102,7 @@ namespace ts { } export function getResolvedModuleFileName(sourceFile: SourceFile, moduleNameText: string): string { - return hasResolvedModuleName(sourceFile, moduleNameText) ? sourceFile.resolvedModules[moduleNameText]: undefined; + return hasResolvedModuleName(sourceFile, moduleNameText) ? sourceFile.resolvedModules[moduleNameText] : undefined; } export function setResolvedModuleName(sourceFile: SourceFile, moduleNameText: string, resolvedFileName: string): void { diff --git a/tests/cases/unittests/reuseProgramStructure.ts b/tests/cases/unittests/reuseProgramStructure.ts index 996f6bb316b45..1cb389f7717fc 100644 --- a/tests/cases/unittests/reuseProgramStructure.ts +++ b/tests/cases/unittests/reuseProgramStructure.ts @@ -3,259 +3,259 @@ /// 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}); - }); - }) + + 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 From e15c70054963cd990d2c99fbaebfd0241bcf2c7b Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 9 Jul 2015 14:45:39 -0700 Subject: [PATCH 9/9] clean old program to prevent it from being captured into the closure --- src/compiler/program.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 2aeab800f0f4a..957b519af5e29 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -188,6 +188,9 @@ namespace ts { verifyCompilerOptions(); + // unconditionally set oldProgram to undefined to prevent it from being captured in closure + oldProgram = undefined; + programTime += new Date().getTime() - start; program = {