Skip to content

Commit 2fac535

Browse files
authored
Fix type-only imports in interface 'extends' and import=/export= (#36496)
* Handle when files get checked in different orders * Fix interface extends clause * Fix import= something type only from a module * Revert apparently unnecessary addition * Revert "Revert apparently unnecessary addition" This reverts commit 7444b0b. * Disallow `import = a.b.c` on anything with type-only imports * Safety first * Add test for TS Server single-file open * Add big comment * Extract error reporting function for import aliases * Delete blank line * Un-export, comment, and colocate some utils * Combine 3 type-only marking function calls into one * Add more export default tests
1 parent aec732a commit 2fac535

31 files changed

+1298
-52
lines changed

src/compiler/checker.ts

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,9 +2221,30 @@ namespace ts {
22212221

22222222
function getTargetOfImportEqualsDeclaration(node: ImportEqualsDeclaration, dontResolveAlias: boolean): Symbol | undefined {
22232223
if (node.moduleReference.kind === SyntaxKind.ExternalModuleReference) {
2224-
return resolveExternalModuleSymbol(resolveExternalModuleName(node, getExternalModuleImportEqualsDeclarationExpression(node)));
2224+
const immediate = resolveExternalModuleName(node, getExternalModuleImportEqualsDeclarationExpression(node));
2225+
const resolved = resolveExternalModuleSymbol(immediate);
2226+
markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false);
2227+
return resolved;
2228+
}
2229+
const resolved = getSymbolOfPartOfRightHandSideOfImportEquals(node.moduleReference, dontResolveAlias);
2230+
checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node, resolved);
2231+
return resolved;
2232+
}
2233+
2234+
function checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node: ImportEqualsDeclaration, resolved: Symbol | undefined) {
2235+
if (markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false)) {
2236+
const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(getSymbolOfNode(node))!;
2237+
const message = typeOnlyDeclaration.kind === SyntaxKind.ExportSpecifier
2238+
? Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_exported_using_export_type
2239+
: Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_imported_using_import_type;
2240+
const relatedMessage = typeOnlyDeclaration.kind === SyntaxKind.ExportSpecifier
2241+
? Diagnostics._0_was_exported_here
2242+
: Diagnostics._0_was_imported_here;
2243+
// Non-null assertion is safe because the optionality comes from ImportClause,
2244+
// but if an ImportClause was the typeOnlyDeclaration, it had to have a `name`.
2245+
const name = unescapeLeadingUnderscores(typeOnlyDeclaration.name!.escapedText);
2246+
addRelatedInfo(error(node.moduleReference, message), createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, name));
22252247
}
2226-
return getSymbolOfPartOfRightHandSideOfImportEquals(node.moduleReference, dontResolveAlias);
22272248
}
22282249

22292250
function resolveExportByName(moduleSymbol: Symbol, name: __String, sourceNode: TypeOnlyCompatibleAliasDeclaration | undefined, dontResolveAlias: boolean) {
@@ -2232,8 +2253,9 @@ namespace ts {
22322253
return getPropertyOfType(getTypeOfSymbol(exportValue), name);
22332254
}
22342255
const exportSymbol = moduleSymbol.exports!.get(name);
2235-
markSymbolOfAliasDeclarationIfResolvesToTypeOnly(sourceNode, exportSymbol);
2236-
return resolveSymbol(exportSymbol, dontResolveAlias);
2256+
const resolved = resolveSymbol(exportSymbol, dontResolveAlias);
2257+
markSymbolOfAliasDeclarationIfTypeOnly(sourceNode, exportSymbol, resolved, /*overwriteEmpty*/ false);
2258+
return resolved;
22372259
}
22382260

22392261
function isSyntacticDefault(node: Node) {
@@ -2273,8 +2295,6 @@ namespace ts {
22732295

22742296
function getTargetOfImportClause(node: ImportClause, dontResolveAlias: boolean): Symbol | undefined {
22752297
const moduleSymbol = resolveExternalModuleName(node, node.parent.moduleSpecifier);
2276-
markSymbolOfAliasDeclarationIfTypeOnly(node);
2277-
22782298
if (moduleSymbol) {
22792299
let exportDefaultSymbol: Symbol | undefined;
22802300
if (isShorthandAmbientModuleSymbol(moduleSymbol)) {
@@ -2316,16 +2336,21 @@ namespace ts {
23162336
}
23172337
else if (hasSyntheticDefault) {
23182338
// per emit behavior, a synthetic default overrides a "real" .default member if `__esModule` is not present
2319-
return resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias);
2339+
const resolved = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias);
2340+
markSymbolOfAliasDeclarationIfTypeOnly(node, moduleSymbol, resolved, /*overwriteTypeOnly*/ false);
2341+
return resolved;
23202342
}
2343+
markSymbolOfAliasDeclarationIfTypeOnly(node, exportDefaultSymbol, /*finalTarget*/ undefined, /*overwriteTypeOnly*/ false);
23212344
return exportDefaultSymbol;
23222345
}
23232346
}
23242347

23252348
function getTargetOfNamespaceImport(node: NamespaceImport, dontResolveAlias: boolean): Symbol | undefined {
23262349
const moduleSpecifier = node.parent.parent.moduleSpecifier;
2327-
markSymbolOfAliasDeclarationIfTypeOnly(node);
2328-
return resolveESModuleSymbol(resolveExternalModuleName(node, moduleSpecifier), moduleSpecifier, dontResolveAlias, /*suppressUsageError*/ false);
2350+
const immediate = resolveExternalModuleName(node, moduleSpecifier);
2351+
const resolved = resolveESModuleSymbol(immediate, moduleSpecifier, dontResolveAlias, /*suppressUsageError*/ false);
2352+
markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false);
2353+
return resolved;
23292354
}
23302355

23312356
function getTargetOfNamespaceExport(node: NamespaceExport, dontResolveAlias: boolean): Symbol | undefined {
@@ -2371,8 +2396,9 @@ namespace ts {
23712396
if (symbol.flags & SymbolFlags.Module) {
23722397
const name = (specifier.propertyName ?? specifier.name).escapedText;
23732398
const exportSymbol = getExportsOfSymbol(symbol).get(name);
2374-
markSymbolOfAliasDeclarationIfResolvesToTypeOnly(specifier, exportSymbol);
2375-
return resolveSymbol(exportSymbol, dontResolveAlias);
2399+
const resolved = resolveSymbol(exportSymbol, dontResolveAlias);
2400+
markSymbolOfAliasDeclarationIfTypeOnly(specifier, exportSymbol, resolved, /*overwriteEmpty*/ false);
2401+
return resolved;
23762402
}
23772403
}
23782404

@@ -2472,24 +2498,28 @@ namespace ts {
24722498
}
24732499

24742500
function getTargetOfImportSpecifier(node: ImportSpecifier, dontResolveAlias: boolean): Symbol | undefined {
2475-
markSymbolOfAliasDeclarationIfTypeOnly(node);
2476-
return getExternalModuleMember(node.parent.parent.parent, node, dontResolveAlias);
2501+
const resolved = getExternalModuleMember(node.parent.parent.parent, node, dontResolveAlias);
2502+
markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false);
2503+
return resolved;
24772504
}
24782505

24792506
function getTargetOfNamespaceExportDeclaration(node: NamespaceExportDeclaration, dontResolveAlias: boolean): Symbol {
24802507
return resolveExternalModuleSymbol(node.parent.symbol, dontResolveAlias);
24812508
}
24822509

24832510
function getTargetOfExportSpecifier(node: ExportSpecifier, meaning: SymbolFlags, dontResolveAlias?: boolean) {
2484-
markSymbolOfAliasDeclarationIfTypeOnly(node);
2485-
return node.parent.parent.moduleSpecifier ?
2511+
const resolved = node.parent.parent.moduleSpecifier ?
24862512
getExternalModuleMember(node.parent.parent, node, dontResolveAlias) :
24872513
resolveEntityName(node.propertyName || node.name, meaning, /*ignoreErrors*/ false, dontResolveAlias);
2514+
markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false);
2515+
return resolved;
24882516
}
24892517

24902518
function getTargetOfExportAssignment(node: ExportAssignment | BinaryExpression, dontResolveAlias: boolean): Symbol | undefined {
24912519
const expression = (isExportAssignment(node) ? node.expression : node.right) as EntityNameExpression | ClassExpression;
2492-
return getTargetOfAliasLikeExpression(expression, dontResolveAlias);
2520+
const resolved = getTargetOfAliasLikeExpression(expression, dontResolveAlias);
2521+
markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false);
2522+
return resolved;
24932523
}
24942524

24952525
function getTargetOfAliasLikeExpression(expression: Expression, dontResolveAlias: boolean) {
@@ -2586,22 +2616,55 @@ namespace ts {
25862616
return links.target;
25872617
}
25882618

2589-
function markSymbolOfAliasDeclarationIfResolvesToTypeOnly(aliasDeclaration: TypeOnlyCompatibleAliasDeclaration | undefined, resolvesToSymbol: Symbol | undefined) {
2590-
if (!aliasDeclaration || !resolvesToSymbol) return;
2619+
/**
2620+
* Marks a symbol as type-only if its declaration is syntactically type-only.
2621+
* If it is not itself marked type-only, but resolves to a type-only alias
2622+
* somewhere in its resolution chain, save a reference to the type-only alias declaration
2623+
* so the alias _not_ marked type-only can be identified as _transitively_ type-only.
2624+
*
2625+
* This function is called on each alias declaration that could be type-only or resolve to
2626+
* another type-only alias during `resolveAlias`, so that later, when an alias is used in a
2627+
* JS-emitting expression, we can quickly determine if that symbol is effectively type-only
2628+
* and issue an error if so.
2629+
*
2630+
* @param aliasDeclaration The alias declaration not marked as type-only
2631+
* has already been marked as not resolving to a type-only alias. Used when recursively resolving qualified
2632+
* names of import aliases, e.g. `import C = a.b.C`. If namespace `a` is not found to be type-only, the
2633+
* import declaration will initially be marked as not resolving to a type-only symbol. But, namespace `b`
2634+
* must still be checked for a type-only marker, overwriting the previous negative result if found.
2635+
* @param immediateTarget The symbol to which the alias declaration immediately resolves
2636+
* @param finalTarget The symbol to which the alias declaration ultimately resolves
2637+
* @param overwriteEmpty Checks `resolvesToSymbol` for type-only declarations even if `aliasDeclaration`
2638+
*/
2639+
function markSymbolOfAliasDeclarationIfTypeOnly(
2640+
aliasDeclaration: Declaration | undefined,
2641+
immediateTarget: Symbol | undefined,
2642+
finalTarget: Symbol | undefined,
2643+
overwriteEmpty: boolean,
2644+
): boolean {
2645+
if (!aliasDeclaration) return false;
2646+
2647+
// If the declaration itself is type-only, mark it and return.
2648+
// No need to check what it resolves to.
25912649
const sourceSymbol = getSymbolOfNode(aliasDeclaration);
2592-
const links = getSymbolLinks(sourceSymbol);
2593-
if (links.typeOnlyDeclaration === undefined) {
2594-
const typeOnly = find(resolvesToSymbol.declarations, isTypeOnlyImportOrExportDeclaration);
2595-
links.typeOnlyDeclaration = typeOnly ?? getSymbolLinks(resolvesToSymbol).typeOnlyDeclaration ?? false;
2650+
if (isTypeOnlyImportOrExportDeclaration(aliasDeclaration)) {
2651+
const links = getSymbolLinks(sourceSymbol);
2652+
links.typeOnlyDeclaration = aliasDeclaration;
2653+
return true;
25962654
}
2655+
2656+
const links = getSymbolLinks(sourceSymbol);
2657+
return markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, immediateTarget, overwriteEmpty)
2658+
|| markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, finalTarget, overwriteEmpty);
25972659
}
25982660

2599-
function markSymbolOfAliasDeclarationIfTypeOnly(aliasDeclaration: TypeOnlyCompatibleAliasDeclaration) {
2600-
if (isTypeOnlyImportOrExportDeclaration(aliasDeclaration)) {
2601-
const symbol = getSymbolOfNode(aliasDeclaration);
2602-
const links = getSymbolLinks(symbol);
2603-
links.typeOnlyDeclaration = aliasDeclaration;
2661+
function markSymbolOfAliasDeclarationIfTypeOnlyWorker(aliasDeclarationLinks: SymbolLinks, target: Symbol | undefined, overwriteEmpty: boolean): boolean {
2662+
if (target && (aliasDeclarationLinks.typeOnlyDeclaration === undefined || overwriteEmpty && aliasDeclarationLinks.typeOnlyDeclaration === false)) {
2663+
const exportSymbol = target.exports?.get(InternalSymbolName.ExportEquals) ?? target;
2664+
const typeOnly = exportSymbol.declarations && find(exportSymbol.declarations, isTypeOnlyImportOrExportDeclaration);
2665+
aliasDeclarationLinks.typeOnlyDeclaration = typeOnly ?? getSymbolLinks(exportSymbol).typeOnlyDeclaration ?? false;
26042666
}
2667+
return !!aliasDeclarationLinks.typeOnlyDeclaration;
26052668
}
26062669

26072670
/** Indicates that a symbol directly or indirectly resolves to a type-only import or export. */
@@ -2739,8 +2802,8 @@ namespace ts {
27392802
throw Debug.assertNever(name, "Unknown entity name kind.");
27402803
}
27412804
Debug.assert((getCheckFlags(symbol) & CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here.");
2742-
if (isIdentifier(name) && symbol.flags & SymbolFlags.Alias) {
2743-
markSymbolOfAliasDeclarationIfResolvesToTypeOnly(getTypeOnlyCompatibleAliasDeclarationFromName(name), symbol);
2805+
if (isEntityName(name) && (symbol.flags & SymbolFlags.Alias || name.parent.kind === SyntaxKind.ExportAssignment)) {
2806+
markSymbolOfAliasDeclarationIfTypeOnly(getAliasDeclarationFromName(name), symbol, /*finalTarget*/ undefined, /*overwriteEmpty*/ true);
27442807
}
27452808
return (symbol.flags & meaning) || dontResolveAlias ? symbol : resolveAlias(symbol);
27462809
}

src/compiler/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2011,4 +2011,4 @@ namespace ts {
20112011
}
20122012
}
20132013
}
2014-
}
2014+
}

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,14 @@
11271127
"category": "Error",
11281128
"code": 1378
11291129
},
1130+
"An import alias cannot reference a declaration that was exported using 'export type'.": {
1131+
"category": "Error",
1132+
"code": 1379
1133+
},
1134+
"An import alias cannot reference a declaration that was imported using 'import type'.": {
1135+
"category": "Error",
1136+
"code": 1380
1137+
},
11301138

11311139
"The types of '{0}' are incompatible between these types.": {
11321140
"category": "Error",

0 commit comments

Comments
 (0)