Skip to content

Commit ab75ea7

Browse files
authored
module resolution: prefer locally defined ambient modules, reuse resolutions to ambient modules from the old program (#11999)
module resolution: prefer locally defined ambient modules, reuse resolutions to ambient modules from the old program
1 parent 7b34b61 commit ab75ea7

File tree

8 files changed

+394
-45
lines changed

8 files changed

+394
-45
lines changed

src/compiler/checker.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ namespace ts {
107107

108108
getJsxElementAttributesType,
109109
getJsxIntrinsicTagNames,
110-
isOptionalParameter
110+
isOptionalParameter,
111+
tryFindAmbientModuleWithoutAugmentations: moduleName => {
112+
// we deliberately exclude augmentations
113+
// since we are only interested in declarations of the module itself
114+
return tryFindAmbientModule(moduleName, /*withAugmentations*/ false);
115+
}
111116
};
112117

113118
const tupleTypes: GenericType[] = [];
@@ -1370,16 +1375,11 @@ namespace ts {
13701375
return;
13711376
}
13721377

1373-
const isRelative = isExternalModuleNameRelative(moduleName);
1374-
const quotedName = '"' + moduleName + '"';
1375-
if (!isRelative) {
1376-
const symbol = getSymbol(globals, quotedName, SymbolFlags.ValueModule);
1377-
if (symbol) {
1378-
// merged symbol is module declaration symbol combined with all augmentations
1379-
return getMergedSymbol(symbol);
1380-
}
1378+
const ambientModule = tryFindAmbientModule(moduleName, /*withAugmentations*/ true);
1379+
if (ambientModule) {
1380+
return ambientModule;
13811381
}
1382-
1382+
const isRelative = isExternalModuleNameRelative(moduleName);
13831383
const resolvedModule = getResolvedModule(getSourceFileOfNode(location), moduleReference);
13841384
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule);
13851385
const sourceFile = resolvedModule && !resolutionDiagnostic && host.getSourceFile(resolvedModule.resolvedFileName);
@@ -4734,6 +4734,15 @@ namespace ts {
47344734
}
47354735
}
47364736

4737+
function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) {
4738+
if (isExternalModuleNameRelative(moduleName)) {
4739+
return undefined;
4740+
}
4741+
const symbol = getSymbol(globals, `"${moduleName}"`, SymbolFlags.ValueModule);
4742+
// merged symbol is module declaration symbol combined with all augmentations
4743+
return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol;
4744+
}
4745+
47374746
function isOptionalParameter(node: ParameterDeclaration) {
47384747
if (hasQuestionToken(node) || isJSDocOptionalParameter(node)) {
47394748
return true;

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2889,6 +2889,14 @@
28892889
"category": "Error",
28902890
"code": 6143
28912891
},
2892+
"Module '{0}' was resolved as locally declared ambient module in file '{1}'.": {
2893+
"category": "Message",
2894+
"code": 6144
2895+
},
2896+
"Module '{0}' was resolved as ambient module declared in '{1}' since this file was not modified.": {
2897+
"category": "Message",
2898+
"code": 6145
2899+
},
28922900
"Variable '{0}' implicitly has an '{1}' type.": {
28932901
"category": "Error",
28942902
"code": 7005

src/compiler/moduleNameResolver.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
/// <reference path="diagnosticInformationMap.generated.ts" />
33

44
namespace ts {
5-
function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void;
6-
function trace(host: ModuleResolutionHost): void {
5+
6+
/* @internal */
7+
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void;
8+
export function trace(host: ModuleResolutionHost): void {
79
host.trace(formatMessage.apply(undefined, arguments));
810
}
911

10-
function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean {
12+
/* @internal */
13+
export function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean {
1114
return compilerOptions.traceResolution && host.trace !== undefined;
1215
}
1316

src/compiler/program.ts

Lines changed: 163 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,130 @@ namespace ts {
462462
return classifiableNames;
463463
}
464464

465+
interface OldProgramState {
466+
program: Program;
467+
file: SourceFile;
468+
modifiedFilePaths: Path[];
469+
}
470+
471+
function resolveModuleNamesReusingOldState(moduleNames: string[], containingFile: string, file: SourceFile, oldProgramState?: OldProgramState) {
472+
if (!oldProgramState && !file.ambientModuleNames.length) {
473+
// if old program state is not supplied and file does not contain locally defined ambient modules
474+
// then the best we can do is fallback to the default logic
475+
return resolveModuleNamesWorker(moduleNames, containingFile);
476+
}
477+
478+
// at this point we know that either
479+
// - file has local declarations for ambient modules
480+
// OR
481+
// - old program state is available
482+
// OR
483+
// - both of items above
484+
// With this it is possible that we can tell how some module names from the initial list will be resolved
485+
// without doing actual resolution (in particular if some name was resolved to ambient module).
486+
// Such names should be excluded from the list of module names that will be provided to `resolveModuleNamesWorker`
487+
// since we don't want to resolve them again.
488+
489+
// this is a list of modules for which we cannot predict resolution so they should be actually resolved
490+
let unknownModuleNames: string[];
491+
// this is a list of combined results assembles from predicted and resolved results.
492+
// Order in this list matches the order in the original list of module names `moduleNames` which is important
493+
// so later we can split results to resolutions of modules and resolutions of module augmentations.
494+
let result: ResolvedModuleFull[];
495+
// a transient placeholder that is used to mark predicted resolution in the result list
496+
const predictedToResolveToAmbientModuleMarker: ResolvedModuleFull = <any>{};
497+
498+
for (let i = 0; i < moduleNames.length; i++) {
499+
const moduleName = moduleNames[i];
500+
// module name is known to be resolved to ambient module if
501+
// - module name is contained in the list of ambient modules that are locally declared in the file
502+
// - in the old program module name was resolved to ambient module whose declaration is in non-modified file
503+
// (so the same module declaration will land in the new program)
504+
let isKnownToResolveToAmbientModule = false;
505+
if (contains(file.ambientModuleNames, moduleName)) {
506+
isKnownToResolveToAmbientModule = true;
507+
if (isTraceEnabled(options, host)) {
508+
trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile);
509+
}
510+
}
511+
else {
512+
isKnownToResolveToAmbientModule = checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName, oldProgramState);
513+
}
514+
515+
if (isKnownToResolveToAmbientModule) {
516+
if (!unknownModuleNames) {
517+
// found a first module name for which result can be prediced
518+
// this means that this module name should not be passed to `resolveModuleNamesWorker`.
519+
// We'll use a separate list for module names that are definitely unknown.
520+
result = new Array(moduleNames.length);
521+
// copy all module names that appear before the current one in the list
522+
// since they are known to be unknown
523+
unknownModuleNames = moduleNames.slice(0, i);
524+
}
525+
// mark prediced resolution in the result list
526+
result[i] = predictedToResolveToAmbientModuleMarker;
527+
}
528+
else if (unknownModuleNames) {
529+
// found unknown module name and we are already using separate list for those - add it to the list
530+
unknownModuleNames.push(moduleName);
531+
}
532+
}
533+
534+
if (!unknownModuleNames) {
535+
// we've looked throught the list but have not seen any predicted resolution
536+
// use default logic
537+
return resolveModuleNamesWorker(moduleNames, containingFile);
538+
}
539+
540+
const resolutions = unknownModuleNames.length
541+
? resolveModuleNamesWorker(unknownModuleNames, containingFile)
542+
: emptyArray;
543+
544+
// combine results of resolutions and predicted results
545+
let j = 0;
546+
for (let i = 0; i < result.length; i++) {
547+
if (result[i] == predictedToResolveToAmbientModuleMarker) {
548+
result[i] = undefined;
549+
}
550+
else {
551+
result[i] = resolutions[j];
552+
j++;
553+
}
554+
}
555+
Debug.assert(j === resolutions.length);
556+
return result;
557+
558+
function checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName: string, oldProgramState?: OldProgramState): boolean {
559+
if (!oldProgramState) {
560+
return false;
561+
}
562+
const resolutionToFile = getResolvedModule(oldProgramState.file, moduleName);
563+
if (resolutionToFile) {
564+
// module used to be resolved to file - ignore it
565+
return false;
566+
}
567+
const ambientModule = oldProgram.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(moduleName);
568+
if (!(ambientModule && ambientModule.declarations)) {
569+
return false;
570+
}
571+
572+
// at least one of declarations should come from non-modified source file
573+
const firstUnmodifiedFile = forEach(ambientModule.declarations, d => {
574+
const f = getSourceFileOfNode(d);
575+
return !contains(oldProgramState.modifiedFilePaths, f.path) && f;
576+
});
577+
578+
if (!firstUnmodifiedFile) {
579+
return false;
580+
}
581+
582+
if (isTraceEnabled(options, host)) {
583+
trace(host, Diagnostics.Module_0_was_resolved_as_ambient_module_declared_in_1_since_this_file_was_not_modified, moduleName, firstUnmodifiedFile.fileName);
584+
}
585+
return true;
586+
}
587+
}
588+
465589
function tryReuseStructureFromOldProgram(): boolean {
466590
if (!oldProgram) {
467591
return false;
@@ -489,7 +613,7 @@ namespace ts {
489613
// check if program source files has changed in the way that can affect structure of the program
490614
const newSourceFiles: SourceFile[] = [];
491615
const filePaths: Path[] = [];
492-
const modifiedSourceFiles: SourceFile[] = [];
616+
const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = [];
493617

494618
for (const oldSourceFile of oldProgram.getSourceFiles()) {
495619
let newSourceFile = host.getSourceFileByPath
@@ -532,29 +656,8 @@ namespace ts {
532656
return false;
533657
}
534658

535-
const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory);
536-
if (resolveModuleNamesWorker) {
537-
const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral);
538-
const resolutions = resolveModuleNamesWorker(moduleNames, newSourceFilePath);
539-
// ensure that module resolution results are still correct
540-
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
541-
if (resolutionsChanged) {
542-
return false;
543-
}
544-
}
545-
if (resolveTypeReferenceDirectiveNamesWorker) {
546-
const typesReferenceDirectives = map(newSourceFile.typeReferenceDirectives, x => x.fileName);
547-
const resolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFilePath);
548-
// ensure that types resolutions are still correct
549-
const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo);
550-
if (resolutionsChanged) {
551-
return false;
552-
}
553-
}
554-
// pass the cache of module/types resolutions from the old source file
555-
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
556-
newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames;
557-
modifiedSourceFiles.push(newSourceFile);
659+
// tentatively approve the file
660+
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
558661
}
559662
else {
560663
// file has no changes - use it as is
@@ -565,6 +668,33 @@ namespace ts {
565668
newSourceFiles.push(newSourceFile);
566669
}
567670

671+
const modifiedFilePaths = modifiedSourceFiles.map(f => f.newFile.path);
672+
// try to verify results of module resolution
673+
for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) {
674+
const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory);
675+
if (resolveModuleNamesWorker) {
676+
const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral);
677+
const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile, { file: oldSourceFile, program: oldProgram, modifiedFilePaths });
678+
// ensure that module resolution results are still correct
679+
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
680+
if (resolutionsChanged) {
681+
return false;
682+
}
683+
}
684+
if (resolveTypeReferenceDirectiveNamesWorker) {
685+
const typesReferenceDirectives = map(newSourceFile.typeReferenceDirectives, x => x.fileName);
686+
const resolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFilePath);
687+
// ensure that types resolutions are still correct
688+
const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo);
689+
if (resolutionsChanged) {
690+
return false;
691+
}
692+
}
693+
// pass the cache of module/types resolutions from the old source file
694+
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
695+
newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames;
696+
}
697+
568698
// update fileName -> file mapping
569699
for (let i = 0, len = newSourceFiles.length; i < len; i++) {
570700
filesByName.set(filePaths[i], newSourceFiles[i]);
@@ -574,7 +704,7 @@ namespace ts {
574704
fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics();
575705

576706
for (const modifiedFile of modifiedSourceFiles) {
577-
fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile);
707+
fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile.newFile);
578708
}
579709
resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives();
580710
oldProgram.structureIsReused = true;
@@ -994,9 +1124,11 @@ namespace ts {
9941124

9951125
const isJavaScriptFile = isSourceFileJavaScript(file);
9961126
const isExternalModuleFile = isExternalModule(file);
1127+
const isDtsFile = isDeclarationFile(file);
9971128

9981129
let imports: LiteralExpression[];
9991130
let moduleAugmentations: LiteralExpression[];
1131+
let ambientModules: string[];
10001132

10011133
// If we are importing helpers, we need to add a synthetic reference to resolve the
10021134
// helpers library.
@@ -1018,6 +1150,7 @@ namespace ts {
10181150

10191151
file.imports = imports || emptyArray;
10201152
file.moduleAugmentations = moduleAugmentations || emptyArray;
1153+
file.ambientModuleNames = ambientModules || emptyArray;
10211154

10221155
return;
10231156

@@ -1053,6 +1186,10 @@ namespace ts {
10531186
(moduleAugmentations || (moduleAugmentations = [])).push(moduleName);
10541187
}
10551188
else if (!inAmbientModule) {
1189+
if (isDtsFile) {
1190+
// for global .d.ts files record name of ambient module
1191+
(ambientModules || (ambientModules = [])).push(moduleName.text);
1192+
}
10561193
// An AmbientExternalModuleDeclaration declares an external module.
10571194
// This type of declaration is permitted only in the global module.
10581195
// The StringLiteral must specify a top - level external module name.
@@ -1298,7 +1435,7 @@ namespace ts {
12981435
if (file.imports.length || file.moduleAugmentations.length) {
12991436
file.resolvedModules = createMap<ResolvedModuleFull>();
13001437
const moduleNames = map(concatenate(file.imports, file.moduleAugmentations), getTextOfLiteral);
1301-
const resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory));
1438+
const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory), file);
13021439
Debug.assert(resolutions.length === moduleNames.length);
13031440
for (let i = 0; i < moduleNames.length; i++) {
13041441
const resolution = resolutions[i];

src/compiler/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,7 @@ namespace ts {
21022102
/* @internal */ imports: LiteralExpression[];
21032103
/* @internal */ moduleAugmentations: LiteralExpression[];
21042104
/* @internal */ patternAmbientModules?: PatternAmbientModule[];
2105+
/* @internal */ ambientModuleNames: string[];
21052106
// The synthesized identifier for an imported external helpers module.
21062107
/* @internal */ externalHelpersModuleName?: Identifier;
21072108
}
@@ -2296,6 +2297,8 @@ namespace ts {
22962297
isOptionalParameter(node: ParameterDeclaration): boolean;
22972298
getAmbientModules(): Symbol[];
22982299

2300+
/* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol;
2301+
22992302
// Should not be called directly. Should only be accessed through the Program instance.
23002303
/* @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[];
23012304
/* @internal */ getGlobalDiagnostics(): Diagnostic[];

0 commit comments

Comments
 (0)