Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module resolution: prefer locally defined ambient modules, reuse resolutions to ambient modules from the old program #11999

Merged
merged 3 commits into from
Nov 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -462,6 +462,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 @@ -489,7 +613,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 @@ -532,29 +656,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 @@ -565,6 +668,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 @@ -574,7 +704,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 @@ -994,9 +1124,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 @@ -1018,6 +1150,7 @@ namespace ts {

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

return;

Expand Down Expand Up @@ -1053,6 +1186,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 @@ -1298,7 +1435,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