Skip to content

Commit

Permalink
module resolution: prefer locally defined ambient modules, reuse reso…
Browse files Browse the repository at this point in the history
…lutions to ambient modules from the old program (#11999)

module resolution: prefer locally defined ambient modules, reuse resolutions to ambient modules from the old program
  • Loading branch information
vladima authored and Andy Hanson committed Nov 3, 2016
1 parent 43dd295 commit 8ecb03e
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 45 deletions.
29 changes: 19 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@ namespace ts {

getJsxElementAttributesType,
getJsxIntrinsicTagNames,
isOptionalParameter
isOptionalParameter,
tryFindAmbientModuleWithoutAugmentations: moduleName => {
// we deliberately exclude augmentations
// since we are only interested in declarations of the module itself
return tryFindAmbientModule(moduleName, /*withAugmentations*/ false);
}
};

const tupleTypes: GenericType[] = [];
Expand Down Expand Up @@ -1370,16 +1375,11 @@ namespace ts {
return;
}

const isRelative = isExternalModuleNameRelative(moduleName);
const quotedName = '"' + moduleName + '"';
if (!isRelative) {
const symbol = getSymbol(globals, quotedName, SymbolFlags.ValueModule);
if (symbol) {
// merged symbol is module declaration symbol combined with all augmentations
return getMergedSymbol(symbol);
}
const ambientModule = tryFindAmbientModule(moduleName, /*withAugmentations*/ true);
if (ambientModule) {
return ambientModule;
}

const isRelative = isExternalModuleNameRelative(moduleName);
const resolvedModule = getResolvedModule(getSourceFileOfNode(location), moduleReference);
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule);
const sourceFile = resolvedModule && !resolutionDiagnostic && host.getSourceFile(resolvedModule.resolvedFileName);
Expand Down Expand Up @@ -4734,6 +4734,15 @@ namespace ts {
}
}

function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) {
if (isExternalModuleNameRelative(moduleName)) {
return undefined;
}
const symbol = getSymbol(globals, `"${moduleName}"`, SymbolFlags.ValueModule);
// merged symbol is module declaration symbol combined with all augmentations
return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol;
}

function isOptionalParameter(node: ParameterDeclaration) {
if (hasQuestionToken(node) || isJSDocOptionalParameter(node)) {
return true;
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2889,6 +2889,14 @@
"category": "Error",
"code": 6143
},
"Module '{0}' was resolved as locally declared ambient module in file '{1}'.": {
"category": "Message",
"code": 6144
},
"Module '{0}' was resolved as ambient module declared in '{1}' since this file was not modified.": {
"category": "Message",
"code": 6145
},
"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
"code": 7005
Expand Down
9 changes: 6 additions & 3 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
/// <reference path="diagnosticInformationMap.generated.ts" />

namespace ts {
function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void;
function trace(host: ModuleResolutionHost): void {

/* @internal */
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void;
export function trace(host: ModuleResolutionHost): void {
host.trace(formatMessage.apply(undefined, arguments));
}

function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean {
/* @internal */
export function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean {
return compilerOptions.traceResolution && host.trace !== undefined;
}

Expand Down
189 changes: 163 additions & 26 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,130 @@ namespace ts {
return classifiableNames;
}

interface OldProgramState {
program: Program;
file: SourceFile;
modifiedFilePaths: Path[];
}

function resolveModuleNamesReusingOldState(moduleNames: string[], containingFile: string, file: SourceFile, oldProgramState?: OldProgramState) {
if (!oldProgramState && !file.ambientModuleNames.length) {
// if old program state is not supplied and file does not contain locally defined ambient modules
// then the best we can do is fallback to the default logic
return resolveModuleNamesWorker(moduleNames, containingFile);
}

// at this point we know that either
// - file has local declarations for ambient modules
// OR
// - old program state is available
// OR
// - both of items above
// With this it is possible that we can tell how some module names from the initial list will be resolved
// without doing actual resolution (in particular if some name was resolved to ambient module).
// Such names should be excluded from the list of module names that will be provided to `resolveModuleNamesWorker`
// since we don't want to resolve them again.

// this is a list of modules for which we cannot predict resolution so they should be actually resolved
let unknownModuleNames: string[];
// this is a list of combined results assembles from predicted and resolved results.
// Order in this list matches the order in the original list of module names `moduleNames` which is important
// so later we can split results to resolutions of modules and resolutions of module augmentations.
let result: ResolvedModuleFull[];
// a transient placeholder that is used to mark predicted resolution in the result list
const predictedToResolveToAmbientModuleMarker: ResolvedModuleFull = <any>{};

for (let i = 0; i < moduleNames.length; i++) {
const moduleName = moduleNames[i];
// module name is known to be resolved to ambient module if
// - module name is contained in the list of ambient modules that are locally declared in the file
// - in the old program module name was resolved to ambient module whose declaration is in non-modified file
// (so the same module declaration will land in the new program)
let isKnownToResolveToAmbientModule = false;
if (contains(file.ambientModuleNames, moduleName)) {
isKnownToResolveToAmbientModule = true;
if (isTraceEnabled(options, host)) {
trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile);
}
}
else {
isKnownToResolveToAmbientModule = checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName, oldProgramState);
}

if (isKnownToResolveToAmbientModule) {
if (!unknownModuleNames) {
// found a first module name for which result can be prediced
// this means that this module name should not be passed to `resolveModuleNamesWorker`.
// We'll use a separate list for module names that are definitely unknown.
result = new Array(moduleNames.length);
// copy all module names that appear before the current one in the list
// since they are known to be unknown
unknownModuleNames = moduleNames.slice(0, i);
}
// mark prediced resolution in the result list
result[i] = predictedToResolveToAmbientModuleMarker;
}
else if (unknownModuleNames) {
// found unknown module name and we are already using separate list for those - add it to the list
unknownModuleNames.push(moduleName);
}
}

if (!unknownModuleNames) {
// we've looked throught the list but have not seen any predicted resolution
// use default logic
return resolveModuleNamesWorker(moduleNames, containingFile);
}

const resolutions = unknownModuleNames.length
? resolveModuleNamesWorker(unknownModuleNames, containingFile)
: emptyArray;

// combine results of resolutions and predicted results
let j = 0;
for (let i = 0; i < result.length; i++) {
if (result[i] == predictedToResolveToAmbientModuleMarker) {
result[i] = undefined;
}
else {
result[i] = resolutions[j];
j++;
}
}
Debug.assert(j === resolutions.length);
return result;

function checkModuleNameResolvedToAmbientModuleInNonModifiedFile(moduleName: string, oldProgramState?: OldProgramState): boolean {
if (!oldProgramState) {
return false;
}
const resolutionToFile = getResolvedModule(oldProgramState.file, moduleName);
if (resolutionToFile) {
// module used to be resolved to file - ignore it
return false;
}
const ambientModule = oldProgram.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(moduleName);
if (!(ambientModule && ambientModule.declarations)) {
return false;
}

// at least one of declarations should come from non-modified source file
const firstUnmodifiedFile = forEach(ambientModule.declarations, d => {
const f = getSourceFileOfNode(d);
return !contains(oldProgramState.modifiedFilePaths, f.path) && f;
});

if (!firstUnmodifiedFile) {
return false;
}

if (isTraceEnabled(options, host)) {
trace(host, Diagnostics.Module_0_was_resolved_as_ambient_module_declared_in_1_since_this_file_was_not_modified, moduleName, firstUnmodifiedFile.fileName);
}
return true;
}
}

function tryReuseStructureFromOldProgram(): boolean {
if (!oldProgram) {
return false;
Expand Down Expand Up @@ -494,7 +618,7 @@ namespace ts {
// check if program source files has changed in the way that can affect structure of the program
const newSourceFiles: SourceFile[] = [];
const filePaths: Path[] = [];
const modifiedSourceFiles: SourceFile[] = [];
const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = [];

for (const oldSourceFile of oldProgram.getSourceFiles()) {
let newSourceFile = host.getSourceFileByPath
Expand Down Expand Up @@ -537,29 +661,8 @@ namespace ts {
return false;
}

const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory);
if (resolveModuleNamesWorker) {
const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral);
const resolutions = resolveModuleNamesWorker(moduleNames, newSourceFilePath);
// ensure that module resolution results are still correct
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
if (resolutionsChanged) {
return false;
}
}
if (resolveTypeReferenceDirectiveNamesWorker) {
const typesReferenceDirectives = map(newSourceFile.typeReferenceDirectives, x => x.fileName);
const resolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFilePath);
// ensure that types resolutions are still correct
const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo);
if (resolutionsChanged) {
return false;
}
}
// pass the cache of module/types resolutions from the old source file
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames;
modifiedSourceFiles.push(newSourceFile);
// tentatively approve the file
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
}
else {
// file has no changes - use it as is
Expand All @@ -570,6 +673,33 @@ namespace ts {
newSourceFiles.push(newSourceFile);
}

const modifiedFilePaths = modifiedSourceFiles.map(f => f.newFile.path);
// try to verify results of module resolution
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);
const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile, { file: oldSourceFile, program: oldProgram, modifiedFilePaths });
// ensure that module resolution results are still correct
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
if (resolutionsChanged) {
return false;
}
}
if (resolveTypeReferenceDirectiveNamesWorker) {
const typesReferenceDirectives = map(newSourceFile.typeReferenceDirectives, x => x.fileName);
const resolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFilePath);
// ensure that types resolutions are still correct
const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo);
if (resolutionsChanged) {
return false;
}
}
// pass the cache of module/types resolutions from the old source file
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames;
}

// update fileName -> file mapping
for (let i = 0, len = newSourceFiles.length; i < len; i++) {
filesByName.set(filePaths[i], newSourceFiles[i]);
Expand All @@ -579,7 +709,7 @@ namespace ts {
fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics();

for (const modifiedFile of modifiedSourceFiles) {
fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile);
fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile.newFile);
}
resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives();
oldProgram.structureIsReused = true;
Expand Down Expand Up @@ -999,9 +1129,11 @@ namespace ts {

const isJavaScriptFile = isSourceFileJavaScript(file);
const isExternalModuleFile = isExternalModule(file);
const isDtsFile = isDeclarationFile(file);

let imports: LiteralExpression[];
let moduleAugmentations: LiteralExpression[];
let ambientModules: string[];

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

file.imports = imports || emptyArray;
file.moduleAugmentations = moduleAugmentations || emptyArray;
file.ambientModuleNames = ambientModules || emptyArray;

return;

Expand Down Expand Up @@ -1058,6 +1191,10 @@ namespace ts {
(moduleAugmentations || (moduleAugmentations = [])).push(moduleName);
}
else if (!inAmbientModule) {
if (isDtsFile) {
// for global .d.ts files record name of ambient module
(ambientModules || (ambientModules = [])).push(moduleName.text);
}
// 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.
Expand Down Expand Up @@ -1303,7 +1440,7 @@ namespace ts {
if (file.imports.length || file.moduleAugmentations.length) {
file.resolvedModules = createMap<ResolvedModuleFull>();
const moduleNames = map(concatenate(file.imports, file.moduleAugmentations), getTextOfLiteral);
const resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory));
const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory), file);
Debug.assert(resolutions.length === moduleNames.length);
for (let i = 0; i < moduleNames.length; i++) {
const resolution = resolutions[i];
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2102,6 +2102,7 @@ namespace ts {
/* @internal */ imports: LiteralExpression[];
/* @internal */ moduleAugmentations: LiteralExpression[];
/* @internal */ patternAmbientModules?: PatternAmbientModule[];
/* @internal */ ambientModuleNames: string[];
// The synthesized identifier for an imported external helpers module.
/* @internal */ externalHelpersModuleName?: Identifier;
}
Expand Down Expand Up @@ -2296,6 +2297,8 @@ namespace ts {
isOptionalParameter(node: ParameterDeclaration): boolean;
getAmbientModules(): Symbol[];

/* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol;

// Should not be called directly. Should only be accessed through the Program instance.
/* @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[];
/* @internal */ getGlobalDiagnostics(): Diagnostic[];
Expand Down
Loading

0 comments on commit 8ecb03e

Please sign in to comment.