Skip to content

Commit

Permalink
Synthesize namespace records for proper esm interop (#19675)
Browse files Browse the repository at this point in the history
* Integrate importStar and importDefault helpers

* Accept baselines

* Support dynamic imports, write helpers for umd module (and amd is possible) kinds

* Accept baselines

* Support AMD, use same helper initialization as is normal

* update typechecker to have errors on called imported namespaces and good error recovery with a quickfix

* Overhaul allowSyntheticDefaultExports to be safer

* Put the new behavior behind a flag

* Rename strictESM to ESMInterop

* ESMInterop -> ESModuleInterop, make default for tsc --init

* Rename ESMInterop -> ESModuleInterop in module.ts, add emit test (since fourslash doesnt do that)

* Remove erroneous semicolons from helper

* Reword diagnostic

* Change style

* Edit followup diagnostic

* Add secondary quickfix for call sites, tests forthcoming

* Add synth default to namespace import type, enhance quickfix

* Pair of spare tests for good measure

* Fix typos in diagnostic message

* Improve comment clarity

* Actually accept the updated changes to the esmodule interop description

* ESModule -> esModule

* Use find and not forEach

* Use guard

* Rely on implicit falsiness of Result.False

* These should have been emit flags
  • Loading branch information
weswigham committed Jan 9, 2018
1 parent 859f0e3 commit 7e63150
Show file tree
Hide file tree
Showing 65 changed files with 1,019 additions and 212 deletions.
135 changes: 120 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,43 @@ namespace ts {
return getSymbolOfPartOfRightHandSideOfImportEquals(<EntityName>node.moduleReference, dontResolveAlias);
}

function resolveExportByName(moduleSymbol: Symbol, name: __String, dontResolveAlias: boolean) {
const exportValue = moduleSymbol.exports.get(InternalSymbolName.ExportEquals);
return exportValue
? getPropertyOfType(getTypeOfSymbol(exportValue), name)
: resolveSymbol(moduleSymbol.exports.get(name), dontResolveAlias);
}

function canHaveSyntheticDefault(file: SourceFile | undefined, moduleSymbol: Symbol, dontResolveAlias: boolean) {
if (!allowSyntheticDefaultImports) {
return false;
}
// Declaration files (and ambient modules)
if (!file || file.isDeclarationFile) {
// Definitely cannot have a synthetic default if they have a default member specified
if (resolveExportByName(moduleSymbol, InternalSymbolName.Default, dontResolveAlias)) {
return false;
}
// It _might_ still be incorrect to assume there is no __esModule marker on the import at runtime, even if there is no `default` member
// So we check a bit more,
if (resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), dontResolveAlias)) {
// If there is an `__esModule` specified in the declaration (meaning someone explicitly added it or wrote it in their code),
// it definitely is a module and does not have a synthetic default
return false;
}
// There are _many_ declaration files not written with esmodules in mind that still get compiled into a format with __esModule set
// Meaning there may be no default at runtime - however to be on the permissive side, we allow access to a synthetic default member
// as there is no marker to indicate if the accompanying JS has `__esModule` or not, or is even native esm
return true;
}
// TypeScript files never have a synthetic default (as they are always emitted with an __esModule marker) _unless_ they contain an export= statement
if (!isSourceFileJavaScript(file)) {
return hasExportAssignmentSymbol(moduleSymbol);
}
// JS files have a synthetic default if they do not contain ES2015+ module syntax (export = is not valid in js) _and_ do not have an __esModule marker
return !file.externalModuleIndicator && !resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), dontResolveAlias);
}

function getTargetOfImportClause(node: ImportClause, dontResolveAlias: boolean): Symbol {
const moduleSymbol = resolveExternalModuleName(node, (<ImportDeclaration>node.parent).moduleSpecifier);

Expand All @@ -1480,16 +1517,16 @@ namespace ts {
exportDefaultSymbol = moduleSymbol;
}
else {
const exportValue = moduleSymbol.exports.get("export=" as __String);
exportDefaultSymbol = exportValue
? getPropertyOfType(getTypeOfSymbol(exportValue), InternalSymbolName.Default)
: resolveSymbol(moduleSymbol.exports.get(InternalSymbolName.Default), dontResolveAlias);
exportDefaultSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, dontResolveAlias);
}

if (!exportDefaultSymbol && !allowSyntheticDefaultImports) {
const file = find(moduleSymbol.declarations, isSourceFile);
const hasSyntheticDefault = canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias);
if (!exportDefaultSymbol && !hasSyntheticDefault) {
error(node.name, Diagnostics.Module_0_has_no_default_export, symbolToString(moduleSymbol));
}
else if (!exportDefaultSymbol && allowSyntheticDefaultImports) {
else if (!exportDefaultSymbol && hasSyntheticDefault) {
// per emit behavior, a synthetic default overrides a "real" .default member if `__esModule` is not present
return resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias);
}
return exportDefaultSymbol;
Expand Down Expand Up @@ -1889,8 +1926,40 @@ namespace ts {
// combine other declarations with the module or variable (e.g. a class/module, function/module, interface/variable).
function resolveESModuleSymbol(moduleSymbol: Symbol, moduleReferenceExpression: Expression, dontResolveAlias: boolean): Symbol {
const symbol = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias);
if (!dontResolveAlias && symbol && !(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable))) {
error(moduleReferenceExpression, Diagnostics.Module_0_resolves_to_a_non_module_entity_and_cannot_be_imported_using_this_construct, symbolToString(moduleSymbol));
if (!dontResolveAlias && symbol) {
if (!(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable))) {
error(moduleReferenceExpression, Diagnostics.Module_0_resolves_to_a_non_module_entity_and_cannot_be_imported_using_this_construct, symbolToString(moduleSymbol));
return symbol;
}
if (compilerOptions.esModuleInterop) {
const referenceParent = moduleReferenceExpression.parent;
if (
(isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) ||
isImportCall(referenceParent)
) {
const type = getTypeOfSymbol(symbol);
let sigs = getSignaturesOfStructuredType(type, SignatureKind.Call);
if (!sigs || !sigs.length) {
sigs = getSignaturesOfStructuredType(type, SignatureKind.Construct);
}
if (sigs && sigs.length) {
const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol);
// Create a new symbol which has the module's type less the call and construct signatures
const result = createSymbol(symbol.flags, symbol.escapedName);
result.declarations = symbol.declarations ? symbol.declarations.slice() : [];
result.parent = symbol.parent;
result.target = symbol;
result.originatingImport = referenceParent;
if (symbol.valueDeclaration) result.valueDeclaration = symbol.valueDeclaration;
if (symbol.constEnumOnlyModule) result.constEnumOnlyModule = true;
if (symbol.members) result.members = cloneMap(symbol.members);
if (symbol.exports) result.exports = cloneMap(symbol.exports);
const resolvedModuleType = resolveStructuredTypeMembers(moduleType as StructuredType); // Should already be resolved from the signature checks above
result.type = createAnonymousType(result, resolvedModuleType.members, emptyArray, emptyArray, resolvedModuleType.stringIndexInfo, resolvedModuleType.numberIndexInfo);
return result;
}
}
}
}
return symbol;
}
Expand Down Expand Up @@ -9452,6 +9521,17 @@ namespace ts {

diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode, errorInfo));
}
// Check if we should issue an extra diagnostic to produce a quickfix for a slightly incorrect import statement
if (headMessage && errorNode && !result && source.symbol) {
const links = getSymbolLinks(source.symbol);
if (links.originatingImport && !isImportCall(links.originatingImport)) {
const helpfulRetry = checkTypeRelatedTo(getTypeOfSymbol(links.target), target, relation, /*errorNode*/ undefined);
if (helpfulRetry) {
// Likely an incorrect import. Issue a helpful diagnostic to produce a quickfix to change the import
diagnostics.add(createDiagnosticForNode(links.originatingImport, Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime));
}
}
}
return result !== Ternary.False;

function reportError(message: DiagnosticMessage, arg0?: string, arg1?: string, arg2?: string): void {
Expand Down Expand Up @@ -17320,7 +17400,7 @@ namespace ts {
error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType));
}
else {
error(node, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
invocationError(node, apparentType, SignatureKind.Call);
}
return resolveErrorCall(node);
}
Expand Down Expand Up @@ -17410,7 +17490,7 @@ namespace ts {
return signature;
}

error(node, Diagnostics.Cannot_use_new_with_an_expression_whose_type_lacks_a_call_or_construct_signature);
invocationError(node, expressionType, SignatureKind.Construct);
return resolveErrorCall(node);
}

Expand Down Expand Up @@ -17457,6 +17537,28 @@ namespace ts {
return true;
}

function invocationError(node: Node, apparentType: Type, kind: SignatureKind) {
error(node, kind === SignatureKind.Call
? Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures
: Diagnostics.Cannot_use_new_with_an_expression_whose_type_lacks_a_call_or_construct_signature
, typeToString(apparentType));
invocationErrorRecovery(apparentType, kind);
}

function invocationErrorRecovery(apparentType: Type, kind: SignatureKind) {
if (!apparentType.symbol) {
return;
}
const importNode = getSymbolLinks(apparentType.symbol).originatingImport;
// Create a diagnostic on the originating import if possible onto which we can attach a quickfix
// An import call expression cannot be rewritten into another form to correct the error - the only solution is to use `.default` at the use-site
if (importNode && !isImportCall(importNode)) {
const sigs = getSignaturesOfType(getTypeOfSymbol(getSymbolLinks(apparentType.symbol).target), kind);
if (!sigs || !sigs.length) return;
error(importNode, Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime);
}
}

function resolveTaggedTemplateExpression(node: TaggedTemplateExpression, candidatesOutArray: Signature[]): Signature {
const tagType = checkExpression(node.tag);
const apparentType = getApparentType(tagType);
Expand All @@ -17474,7 +17576,7 @@ namespace ts {
}

if (!callSignatures.length) {
error(node, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
invocationError(node, apparentType, SignatureKind.Call);
return resolveErrorCall(node);
}

Expand Down Expand Up @@ -17531,6 +17633,7 @@ namespace ts {
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
errorInfo = chainDiagnosticMessages(errorInfo, headMessage);
diagnostics.add(createDiagnosticForNodeFromMessageChain(node, errorInfo));
invocationErrorRecovery(apparentType, SignatureKind.Call);
return resolveErrorCall(node);
}

Expand Down Expand Up @@ -17785,25 +17888,27 @@ namespace ts {
if (moduleSymbol) {
const esModuleSymbol = resolveESModuleSymbol(moduleSymbol, specifier, /*dontRecursivelyResolve*/ true);
if (esModuleSymbol) {
return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol));
return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol));
}
}
return createPromiseReturnType(node, anyType);
}

function getTypeWithSyntheticDefaultImportType(type: Type, symbol: Symbol): Type {
function getTypeWithSyntheticDefaultImportType(type: Type, symbol: Symbol, originalSymbol: Symbol): Type {
if (allowSyntheticDefaultImports && type && type !== unknownType) {
const synthType = type as SyntheticDefaultModuleType;
if (!synthType.syntheticType) {
if (!getPropertyOfType(type, InternalSymbolName.Default)) {
const file = find(originalSymbol.declarations, isSourceFile);
const hasSyntheticDefault = canHaveSyntheticDefault(file, originalSymbol, /*dontResolveAlias*/ false);
if (hasSyntheticDefault) {
const memberTable = createSymbolTable();
const newSymbol = createSymbol(SymbolFlags.Alias, InternalSymbolName.Default);
newSymbol.target = resolveSymbol(symbol);
memberTable.set(InternalSymbolName.Default, newSymbol);
const anonymousSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type);
const defaultContainingObject = createAnonymousType(anonymousSymbol, memberTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined);
anonymousSymbol.type = defaultContainingObject;
synthType.syntheticType = getIntersectionType([type, defaultContainingObject]);
synthType.syntheticType = (type.flags & TypeFlags.StructuredType && type.symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable)) ? getSpreadType(type, defaultContainingObject, anonymousSymbol, /*propegatedFlags*/ 0) : defaultContainingObject;
}
else {
synthType.syntheticType = type;
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,13 @@ namespace ts {
category: Diagnostics.Module_Resolution_Options,
description: Diagnostics.Allow_default_imports_from_modules_with_no_default_export_This_does_not_affect_code_emit_just_typechecking
},
{
name: "esModuleInterop",
type: "boolean",
showInSimplifiedHelpView: true,
category: Diagnostics.Module_Resolution_Options,
description: Diagnostics.Enables_emit_interoperability_between_CommonJS_and_ES_Modules_via_creation_of_namespace_objects_for_all_imports_Implies_allowSyntheticDefaultImports
},
{
name: "preserveSymlinks",
type: "boolean",
Expand Down Expand Up @@ -704,7 +711,8 @@ namespace ts {
export const defaultInitCompilerOptions: CompilerOptions = {
module: ModuleKind.CommonJS,
target: ScriptTarget.ES5,
strict: true
strict: true,
esModuleInterop: true
};

let optionNameMapCache: OptionNameMap;
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2005,7 +2005,9 @@ namespace ts {
const moduleKind = getEmitModuleKind(compilerOptions);
return compilerOptions.allowSyntheticDefaultImports !== undefined
? compilerOptions.allowSyntheticDefaultImports
: moduleKind === ModuleKind.System;
: compilerOptions.esModuleInterop
? moduleKind !== ModuleKind.None && moduleKind < ModuleKind.ES2015
: moduleKind === ModuleKind.System;
}

export type StrictOptionName = "noImplicitAny" | "noImplicitThis" | "strictNullChecks" | "strictFunctionTypes" | "strictPropertyInitialization" | "alwaysStrict";
Expand Down
16 changes: 16 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3548,6 +3548,14 @@
"category": "Error",
"code": 7036
},
"Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'.": {
"category": "Message",
"code": 7037
},
"A namespace-style import cannot be called or constructed, and will cause a failure at runtime.": {
"category": "Error",
"code": 7038
},

"You cannot rename this element.": {
"category": "Error",
Expand Down Expand Up @@ -3906,5 +3914,13 @@
"Install '{0}'": {
"category": "Message",
"code": 95014
},
"Replace import with '{0}'.": {
"category": "Message",
"code": 95015
},
"Use synthetic 'default' member.": {
"category": "Message",
"code": 95016
}
}
1 change: 1 addition & 0 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,7 @@ namespace ts {
// synthesize 'import "tslib"' declaration
const externalHelpersModuleReference = createLiteral(externalHelpersModuleNameText);
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*importClause*/ undefined);
addEmitFlags(importDecl, EmitFlags.NeverApplyImportHelper);
externalHelpersModuleReference.parent = importDecl;
importDecl.parent = file;
imports = [externalHelpersModuleReference];
Expand Down
14 changes: 7 additions & 7 deletions src/compiler/transformers/module/es2015.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ namespace ts {
if (externalHelpersModuleName) {
const statements: Statement[] = [];
const statementOffset = addPrologue(statements, node.statements);
append(statements,
createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createImportClause(/*name*/ undefined, createNamespaceImport(externalHelpersModuleName)),
createLiteral(externalHelpersModuleNameText)
)
const tslibImport = createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createImportClause(/*name*/ undefined, createNamespaceImport(externalHelpersModuleName)),
createLiteral(externalHelpersModuleNameText)
);
addEmitFlags(tslibImport, EmitFlags.NeverApplyImportHelper);
append(statements, tslibImport);

addRange(statements, visitNodes(node.statements, visitor, isStatement, statementOffset));
return updateSourceFileNode(
Expand Down
Loading

0 comments on commit 7e63150

Please sign in to comment.