Skip to content

Commit 6547c18

Browse files
committed
check ambient modules before reusing old state
1 parent ac20fc2 commit 6547c18

5 files changed

Lines changed: 77 additions & 69 deletions

File tree

src/compiler/diagnosticMessages.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3201,9 +3201,13 @@
32013201
"category": "Message",
32023202
"code": 6182
32033203
},
3204-
"Reusing module resolutions originating in '{0}' since this file was not modified.": {
3204+
"Reusing resolution of module '{0}' to file '{1}' from old program.": {
32053205
"category": "Message",
3206-
"code": 6183
3206+
"code": 618
3207+
},
3208+
"Reusing module resolutions originating in '{0}' since resolutions are unchanged from old program.": {
3209+
"category": "Message",
3210+
"code": 6184
32073211
},
32083212
"Variable '{0}' implicitly has an '{1}' type.": {
32093213
"category": "Error",

src/compiler/program.ts

Lines changed: 67 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ namespace ts {
480480
interface OldProgramState {
481481
program: Program | undefined;
482482
file: SourceFile;
483+
/** The collection of paths modified *since* the old program. */
483484
modifiedFilePaths: Path[];
484485
}
485486

@@ -497,101 +498,96 @@ namespace ts {
497498
return resolveModuleNamesWorker(moduleNames, containingFile);
498499
}
499500

500-
const oldSourceFile = oldProgramState.program && oldProgramState.program.getSourceFile(containingFile);
501-
if (oldSourceFile === file) {
502-
// `file` is unchanged from the old program, so we can reuse old module resolutions.
503-
if (isTraceEnabled(options, host)) {
504-
trace(host, Diagnostics.Reusing_module_resolutions_originating_in_0_since_this_file_was_not_modified, file.path);
505-
}
506-
const oldSourceFileResult: ResolvedModuleFull[] = [];
507-
for (const moduleName of moduleNames) {
508-
const resolvedModule = oldSourceFile.resolvedModules.get(moduleName);
509-
oldSourceFileResult.push(resolvedModule);
510-
}
511-
return oldSourceFileResult;
512-
}
513-
514-
// at this point we know that either
501+
// at this point we know at least one of the following hold:
515502
// - file has local declarations for ambient modules
516-
// OR
517503
// - old program state is available
518-
// OR
519-
// - both of items above
520-
// With this it is possible that we can tell how some module names from the initial list will be resolved
521-
// without doing actual resolution (in particular if some name was resolved to ambient module).
522-
// Such names should be excluded from the list of module names that will be provided to `resolveModuleNamesWorker`
523-
// since we don't want to resolve them again.
524-
525-
// this is a list of modules for which we cannot predict resolution so they should be actually resolved
504+
// With this information, we can infer some module resolutions without performing resolution.
505+
506+
/** An ordered list of module names for which we cannot recover the resolution. */
526507
let unknownModuleNames: string[];
527-
// this is a list of combined results assembles from predicted and resolved results.
528-
// Order in this list matches the order in the original list of module names `moduleNames` which is important
529-
// so later we can split results to resolutions of modules and resolutions of module augmentations.
508+
/**
509+
* The indexing of elements in this list matches that of `moduleNames`.
510+
*
511+
* Before combining results, result[i] is in one of the following states:
512+
* * undefined: needs to be recomputed,
513+
* * predictedToResolveToAmbientModuleMarker: known to be an ambient module.
514+
* Needs to be reset to undefined before returning,
515+
* * ResolvedModuleFull instance: can be reused.
516+
*/
530517
let result: ResolvedModuleFull[];
531-
// a transient placeholder that is used to mark predicted resolution in the result list
518+
/** A transient placeholder used to mark predicted resolution in the result list. */
532519
const predictedToResolveToAmbientModuleMarker: ResolvedModuleFull = <any>{};
533520

521+
const oldSourceFile = oldProgramState.program && oldProgramState.program.getSourceFile(containingFile);
522+
534523
for (let i = 0; i < moduleNames.length; i++) {
535524
const moduleName = moduleNames[i];
536-
// module name is known to be resolved to ambient module if
537-
// - module name is contained in the list of ambient modules that are locally declared in the file
538-
// - in the old program module name was resolved to ambient module whose declaration is in non-modified file
525+
// TODO: if we want to reuse resolutions more aggressively, refine this to check for whether the
526+
// text of the corresponding modulenames has changed.
527+
if (file === oldSourceFile) {
528+
const oldResolvedModule = oldSourceFile && oldSourceFile.resolvedModules.get(moduleName);
529+
if (oldResolvedModule) {
530+
if (isTraceEnabled(options, host)) {
531+
trace(host, Diagnostics.Reusing_resolution_of_module_0_to_file_1_from_old_program, moduleName, containingFile);
532+
}
533+
(result || (result = new Array(moduleNames.length)))[i] = oldResolvedModule;
534+
continue;
535+
}
536+
}
537+
// We know moduleName resolves to an ambient module provided that moduleName:
538+
// - is in the list of ambient modules locally declared in the current source file.
539+
// - resolved to an ambient module in the old program whose declaration is in an unmodified file
539540
// (so the same module declaration will land in the new program)
540-
let isKnownToResolveToAmbientModule = false;
541+
let resolvesToAmbientModuleInNonModifiedFile = false;
541542
if (contains(file.ambientModuleNames, moduleName)) {
542-
isKnownToResolveToAmbientModule = true;
543+
resolvesToAmbientModuleInNonModifiedFile = true;
543544
if (isTraceEnabled(options, host)) {
544545
trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile);
545546
}
546547
}
547548
else {
548-
isKnownToResolveToAmbientModule = checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName, oldProgramState);
549-
}
550-
551-
if (isKnownToResolveToAmbientModule) {
552-
if (!unknownModuleNames) {
553-
// found a first module name for which result can be prediced
554-
// this means that this module name should not be passed to `resolveModuleNamesWorker`.
555-
// We'll use a separate list for module names that are definitely unknown.
556-
result = new Array(moduleNames.length);
557-
// copy all module names that appear before the current one in the list
558-
// since they are known to be unknown
559-
unknownModuleNames = moduleNames.slice(0, i);
560-
}
561-
// Mark predicted resolution in the result list.
562-
result[i] = predictedToResolveToAmbientModuleMarker;
549+
resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, oldProgramState);
563550
}
564-
else if (unknownModuleNames) {
565-
// found unknown module name and we are already using separate list for those - add it to the list
566-
unknownModuleNames.push(moduleName);
567-
}
568-
}
569551

570-
if (!unknownModuleNames) {
571-
// we've looked throught the list but have not seen any predicted resolution
572-
// use default logic
573-
return resolveModuleNamesWorker(moduleNames, containingFile);
552+
if (resolvesToAmbientModuleInNonModifiedFile) {
553+
(result || (result = new Array(moduleNames.length)))[i] = predictedToResolveToAmbientModuleMarker;
554+
}
555+
else {
556+
// Resolution failed in the old program, or resolved to an ambient module for which we can't reuse the result.
557+
(unknownModuleNames || (unknownModuleNames = [])).push(moduleName);
558+
}
574559
}
575560

576-
const resolutions = unknownModuleNames.length
561+
const resolutions = unknownModuleNames && unknownModuleNames.length
577562
? resolveModuleNamesWorker(unknownModuleNames, containingFile)
578563
: emptyArray;
579564

580-
// combine results of resolutions and predicted results
565+
// Combine results of resolutions and predicted results
566+
if (!result) {
567+
// There were no unresolved/ambient resolutions.
568+
Debug.assert(resolutions.length === moduleNames.length);
569+
return <ResolvedModuleFull[]>resolutions;
570+
}
571+
581572
let j = 0;
582573
for (let i = 0; i < result.length; i++) {
583-
if (result[i] === predictedToResolveToAmbientModuleMarker) {
584-
result[i] = undefined;
574+
if (result[i]) {
575+
if (result[i] === predictedToResolveToAmbientModuleMarker) {
576+
result[i] = undefined;
577+
}
585578
}
586579
else {
587580
result[i] = resolutions[j];
588581
j++;
589582
}
590583
}
591584
Debug.assert(j === resolutions.length);
585+
592586
return result;
593587

594-
function checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName: string, oldProgramState: OldProgramState): boolean {
588+
// TODO: if we change our policy of rechecking failed lookups on each program create,
589+
// we should adjust the value returned here.
590+
function moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName: string, oldProgramState: OldProgramState): boolean {
595591
const resolutionToFile = getResolvedModule(oldProgramState.file, moduleName);
596592
if (resolutionToFile) {
597593
// module used to be resolved to file - ignore it
@@ -706,7 +702,7 @@ namespace ts {
706702

707703
modifiedFilePaths = modifiedSourceFiles.map(f => f.newFile.path);
708704
// try to verify results of module resolution
709-
for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) {
705+
modifiedSourceFilesLoop: for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) {
710706
const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory);
711707
if (resolveModuleNamesWorker) {
712708
const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral);
@@ -715,7 +711,8 @@ namespace ts {
715711
// ensure that module resolution results are still correct
716712
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
717713
if (resolutionsChanged) {
718-
return oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles;
714+
oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles;
715+
continue modifiedSourceFilesLoop;
719716
}
720717
}
721718
if (resolveTypeReferenceDirectiveNamesWorker) {
@@ -724,14 +721,19 @@ namespace ts {
724721
// ensure that types resolutions are still correct
725722
const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo);
726723
if (resolutionsChanged) {
727-
return oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles;
724+
oldProgram.structureIsReused = StructureIsReused.ModulesInUneditedFiles;
725+
continue modifiedSourceFilesLoop;
728726
}
729727
}
730728
// pass the cache of module/types resolutions from the old source file
731729
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
732730
newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames;
733731
}
734732

733+
if (oldProgram.structureIsReused !== StructureIsReused.Completely) {
734+
return oldProgram.structureIsReused;
735+
}
736+
735737
// update fileName -> file mapping
736738
for (let i = 0; i < newSourceFiles.length; i++) {
737739
filesByName.set(filePaths[i], newSourceFiles[i]);

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,6 +2409,7 @@ namespace ts {
24092409
/* @internal */ structureIsReused?: StructureIsReused;
24102410
}
24112411

2412+
/* @internal */
24122413
export const enum StructureIsReused {
24132414
Completely,
24142415
ModulesInUneditedFiles,

src/compiler/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ namespace ts {
123123
/* @internal */
124124
export function hasChangesInResolutions<T>(names: string[], newResolutions: T[], oldResolutions: Map<T>, comparer: (oldResolution: T, newResolution: T) => boolean): boolean {
125125
if (names.length !== newResolutions.length) {
126-
return false;
126+
return true;
127127
}
128128
for (let i = 0; i < names.length; i++) {
129129
const newResolution = newResolutions[i];

src/harness/unittests/reuseProgramStructure.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,8 @@ namespace ts {
547547
"File 'node_modules/@types/typerefs2/package.json' does not exist.",
548548
"File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.",
549549
"======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========",
550-
"Reusing module resolutions originating in 'f2.ts' since this file was not modified."
550+
"Reusing resolution of module './b2' to file 'f2.ts' from old program.",
551+
"Reusing resolution of module './f1' to file 'f2.ts' from old program."
551552
], "should reuse module resolutions in f2 since it is unchanged");
552553
});
553554
});

0 commit comments

Comments
 (0)