Skip to content

Commit 8ecb03e

Browse files
vladimaAndy Hanson
authored andcommitted
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 43dd295 commit 8ecb03e

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
@@ -467,6 +467,130 @@ namespace ts {
467467
return classifiableNames;
468468
}
469469

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

499623
for (const oldSourceFile of oldProgram.getSourceFiles()) {
500624
let newSourceFile = host.getSourceFileByPath
@@ -537,29 +661,8 @@ namespace ts {
537661
return false;
538662
}
539663

540-
const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory);
541-
if (resolveModuleNamesWorker) {
542-
const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral);
543-
const resolutions = resolveModuleNamesWorker(moduleNames, newSourceFilePath);
544-
// ensure that module resolution results are still correct
545-
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
546-
if (resolutionsChanged) {
547-
return false;
548-
}
549-
}
550-
if (resolveTypeReferenceDirectiveNamesWorker) {
551-
const typesReferenceDirectives = map(newSourceFile.typeReferenceDirectives, x => x.fileName);
552-
const resolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFilePath);
553-
// ensure that types resolutions are still correct
554-
const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo);
555-
if (resolutionsChanged) {
556-
return false;
557-
}
558-
}
559-
// pass the cache of module/types resolutions from the old source file
560-
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
561-
newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames;
562-
modifiedSourceFiles.push(newSourceFile);
664+
// tentatively approve the file
665+
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
563666
}
564667
else {
565668
// file has no changes - use it as is
@@ -570,6 +673,33 @@ namespace ts {
570673
newSourceFiles.push(newSourceFile);
571674
}
572675

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

581711
for (const modifiedFile of modifiedSourceFiles) {
582-
fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile);
712+
fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile.newFile);
583713
}
584714
resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives();
585715
oldProgram.structureIsReused = true;
@@ -999,9 +1129,11 @@ namespace ts {
9991129

10001130
const isJavaScriptFile = isSourceFileJavaScript(file);
10011131
const isExternalModuleFile = isExternalModule(file);
1132+
const isDtsFile = isDeclarationFile(file);
10021133

10031134
let imports: LiteralExpression[];
10041135
let moduleAugmentations: LiteralExpression[];
1136+
let ambientModules: string[];
10051137

10061138
// If we are importing helpers, we need to add a synthetic reference to resolve the
10071139
// helpers library.
@@ -1023,6 +1155,7 @@ namespace ts {
10231155

10241156
file.imports = imports || emptyArray;
10251157
file.moduleAugmentations = moduleAugmentations || emptyArray;
1158+
file.ambientModuleNames = ambientModules || emptyArray;
10261159

10271160
return;
10281161

@@ -1058,6 +1191,10 @@ namespace ts {
10581191
(moduleAugmentations || (moduleAugmentations = [])).push(moduleName);
10591192
}
10601193
else if (!inAmbientModule) {
1194+
if (isDtsFile) {
1195+
// for global .d.ts files record name of ambient module
1196+
(ambientModules || (ambientModules = [])).push(moduleName.text);
1197+
}
10611198
// An AmbientExternalModuleDeclaration declares an external module.
10621199
// This type of declaration is permitted only in the global module.
10631200
// The StringLiteral must specify a top - level external module name.
@@ -1303,7 +1440,7 @@ namespace ts {
13031440
if (file.imports.length || file.moduleAugmentations.length) {
13041441
file.resolvedModules = createMap<ResolvedModuleFull>();
13051442
const moduleNames = map(concatenate(file.imports, file.moduleAugmentations), getTextOfLiteral);
1306-
const resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory));
1443+
const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory), file);
13071444
Debug.assert(resolutions.length === moduleNames.length);
13081445
for (let i = 0; i < moduleNames.length; i++) {
13091446
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)