diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 3c867024485..a024a574a93 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -251,6 +251,15 @@ namespace ts { } } + export function zipToMap(keys: string[], values: T[]): Map { + Debug.assert(keys.length === values.length); + const map = createMap(); + for (let i = 0; i < keys.length; ++i) { + map.set(keys[i], values[i]); + } + return map; + } + /** * Iterates through `array` by index and performs the callback on each element of array until the callback * returns a falsey value, then returns false. diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 63d257c359a..c44fdce391e 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -491,7 +491,24 @@ namespace ts { return resolveModuleNamesWorker(moduleNames, containingFile); } - // at this point we know at least one of the following hold: + const oldSourceFile = oldProgramState.program && oldProgramState.program.getSourceFile(containingFile); + if (oldSourceFile !== file && file.resolvedModules) { + // `file` was created for the new program. + // + // We only set `file.resolvedModules` via work from the current function, + // so it is defined iff we already called the current function on `file`. + // That call happened no later than the creation of the `file` object, + // which per above occured during the current program creation. + // Since we model program creation as an atomic operation w/r/t IO, + // it is safe to reuse resolutions from the earlier call. + const result: ResolvedModuleFull[] = []; + for (const moduleName of moduleNames) { + const resolvedModule = file.resolvedModules.get(moduleName); + result.push(resolvedModule); + } + return result; + } + // At this point, we know at least one of the following hold: // - file has local declarations for ambient modules // - old program state is available // With this information, we can infer some module resolutions without performing resolution. @@ -511,7 +528,6 @@ namespace ts { /** A transient placeholder used to mark predicted resolution in the result list. */ const predictedToResolveToAmbientModuleMarker: ResolvedModuleFull = {}; - const oldSourceFile = oldProgramState.program && oldProgramState.program.getSourceFile(containingFile); for (let i = 0; i < moduleNames.length; i++) { const moduleName = moduleNames[i]; @@ -620,7 +636,7 @@ namespace ts { return oldProgram.structureIsReused = StructureIsReused.Not; } - Debug.assert(!(oldProgram.structureIsReused & (StructureIsReused.Completely | StructureIsReused.ModulesInUneditedFiles))); + Debug.assert(!(oldProgram.structureIsReused & (StructureIsReused.Completely | StructureIsReused.SafeModules))); // there is an old program, check if we can reuse its structure const oldRootNames = oldProgram.getRootFileNames(); @@ -653,32 +669,34 @@ namespace ts { filePaths.push(newSourceFile.path); if (oldSourceFile !== newSourceFile) { + // The `newSourceFile` object was created for the new program. + 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 - oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } // check tripleslash references if (!arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { // tripleslash references has changed - oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } // check imports and module augmentations collectExternalModuleReferences(newSourceFile); if (!arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { // imports has changed - oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } if (!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations, moduleNameIsEqualTo)) { // moduleAugmentations has changed - oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } if (!arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { // 'types' references has changed - oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles; + oldProgram.structureIsReused = StructureIsReused.SafeModules; } // tentatively approve the file @@ -695,7 +713,7 @@ namespace ts { modifiedFilePaths = modifiedSourceFiles.map(f => f.newFile.path); // try to verify results of module resolution - modifiedSourceFilesLoop: for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) { + for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) { const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory); if (resolveModuleNamesWorker) { const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral); @@ -704,8 +722,11 @@ namespace ts { // ensure that module resolution results are still correct const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo); if (resolutionsChanged) { - oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles; - continue modifiedSourceFilesLoop; + oldProgram.structureIsReused = StructureIsReused.SafeModules; + newSourceFile.resolvedModules = zipToMap(moduleNames, resolutions); + } + else { + newSourceFile.resolvedModules = oldSourceFile.resolvedModules; } } if (resolveTypeReferenceDirectiveNamesWorker) { @@ -714,13 +735,13 @@ namespace ts { // ensure that types resolutions are still correct const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo); if (resolutionsChanged) { - oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles; - continue modifiedSourceFilesLoop; + oldProgram.structureIsReused = StructureIsReused.SafeModules; + newSourceFile.resolvedTypeReferenceDirectiveNames = zipToMap(typesReferenceDirectives, resolutions); + } + else { + newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; } } - // pass the cache of module/types resolutions from the old source file - newSourceFile.resolvedModules = oldSourceFile.resolvedModules; - newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; } if (oldProgram.structureIsReused !== StructureIsReused.Completely) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4c850d673a9..55b7222796d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2412,7 +2412,7 @@ namespace ts { /* @internal */ export const enum StructureIsReused { Completely, - ModulesInUneditedFiles, + SafeModules, Not } diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index d7140cfc8b2..41d7045064f 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -261,7 +261,7 @@ namespace ts { `; files[0].text = files[0].text.updateReferences(newReferences); }); - assert.isTrue(program_1.structureIsReused === StructureIsReused.ModulesInUneditedFiles); + assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules); }); it("fails if change affects type references", () => { @@ -281,7 +281,7 @@ namespace ts { updateProgram(program_1, ["a.ts"], { target }, files => { files[2].text = files[2].text.updateImportsAndExports("import x from 'b'"); }); - assert.isTrue(program_1.structureIsReused === StructureIsReused.ModulesInUneditedFiles); + assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules); }); it("fails if change affects type directives", () => { @@ -293,7 +293,7 @@ namespace ts { /// `; files[0].text = files[0].text.updateReferences(newReferences); }); - assert.isTrue(program_1.structureIsReused === StructureIsReused.ModulesInUneditedFiles); + assert.isTrue(program_1.structureIsReused === StructureIsReused.SafeModules); }); it("fails if module kind changes", () => { @@ -340,7 +340,7 @@ namespace ts { const program_3 = updateProgram(program_2, ["a.ts"], options, files => { files[0].text = files[0].text.updateImportsAndExports(""); }); - assert.isTrue(program_2.structureIsReused === StructureIsReused.ModulesInUneditedFiles); + assert.isTrue(program_2.structureIsReused === StructureIsReused.SafeModules); checkResolvedModulesCache(program_3, "a.ts", /*expectedContent*/ undefined); const program_4 = updateProgram(program_3, ["a.ts"], options, files => { @@ -349,7 +349,7 @@ namespace ts { `; files[0].text = files[0].text.updateImportsAndExports(newImports); }); - assert.isTrue(program_3.structureIsReused === StructureIsReused.ModulesInUneditedFiles); + assert.isTrue(program_3.structureIsReused === StructureIsReused.SafeModules); checkResolvedModulesCache(program_4, "a.ts", createMapFromTemplate({ "b": createResolvedModule("b.ts"), "c": undefined })); }); @@ -378,7 +378,7 @@ namespace ts { files[0].text = files[0].text.updateReferences(""); }); - assert.isTrue(program_2.structureIsReused === StructureIsReused.ModulesInUneditedFiles); + assert.isTrue(program_2.structureIsReused === StructureIsReused.SafeModules); checkResolvedTypeDirectivesCache(program_3, "/a.ts", /*expectedContent*/ undefined); updateProgram(program_3, ["/a.ts"], options, files => { @@ -387,7 +387,7 @@ namespace ts { `; files[0].text = files[0].text.updateReferences(newReferences); }); - assert.isTrue(program_3.structureIsReused === StructureIsReused.ModulesInUneditedFiles); + assert.isTrue(program_3.structureIsReused === StructureIsReused.SafeModules); checkResolvedTypeDirectivesCache(program_1, "/a.ts", createMapFromTemplate({ "typedefs": { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); });