Skip to content

Commit 8610ff5

Browse files
authored
Add --preserveValueImports (#44619)
* Add compiler option * Require es2015+ * Do not elide any imports or exports in preserve-exact * Add errors for writing imports/exports that reference elided names * Improve diagnostics wording * Update API baselines * Redo as noEraslingImportedNames * Update option category * Update baselines * Lint * Fix up transformer comments * Fix errors from merge * Update other error code baseline * Rename to "preserveValueImports" * Clean up, reword diagnostics * Update API baselines * Update other baseline affected by error message reword * Update tsconfig baselines * Add debug assertion instead of !
1 parent b9eeb74 commit 8610ff5

File tree

55 files changed

+1367
-40
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1367
-40
lines changed

src/compiler/checker.ts

+62-15
Original file line numberDiff line numberDiff line change
@@ -2198,17 +2198,25 @@ namespace ts {
21982198
const message = isExport
21992199
? Diagnostics._0_cannot_be_used_as_a_value_because_it_was_exported_using_export_type
22002200
: Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type;
2201-
const relatedMessage = isExport
2202-
? Diagnostics._0_was_exported_here
2203-
: Diagnostics._0_was_imported_here;
22042201
const unescapedName = unescapeLeadingUnderscores(name);
2205-
addRelatedInfo(
2202+
addTypeOnlyDeclarationRelatedInfo(
22062203
error(useSite, message, unescapedName),
2207-
createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, unescapedName));
2204+
typeOnlyDeclaration,
2205+
unescapedName);
22082206
}
22092207
}
22102208
}
22112209

2210+
function addTypeOnlyDeclarationRelatedInfo(diagnostic: Diagnostic, typeOnlyDeclaration: TypeOnlyCompatibleAliasDeclaration | undefined, unescapedName: string) {
2211+
if (!typeOnlyDeclaration) return diagnostic;
2212+
return addRelatedInfo(
2213+
diagnostic,
2214+
createDiagnosticForNode(
2215+
typeOnlyDeclaration,
2216+
typeOnlyDeclarationIsExport(typeOnlyDeclaration) ? Diagnostics._0_was_exported_here : Diagnostics._0_was_imported_here,
2217+
unescapedName));
2218+
}
2219+
22122220
function getIsDeferredContext(location: Node, lastLocation: Node | undefined): boolean {
22132221
if (location.kind !== SyntaxKind.ArrowFunction && location.kind !== SyntaxKind.FunctionExpression) {
22142222
// initializers in instance property declaration of class like entities are executed in constructor and thus deferred
@@ -38813,13 +38821,49 @@ namespace ts {
3881338821
error(node, message, symbolToString(symbol));
3881438822
}
3881538823

38816-
// Don't allow to re-export something with no value side when `--isolatedModules` is set.
3881738824
if (compilerOptions.isolatedModules
38818-
&& node.kind === SyntaxKind.ExportSpecifier
38819-
&& !node.parent.parent.isTypeOnly
38820-
&& !(target.flags & SymbolFlags.Value)
38825+
&& !isTypeOnlyImportOrExportDeclaration(node)
3882138826
&& !(node.flags & NodeFlags.Ambient)) {
38822-
error(node, Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type);
38827+
const typeOnlyAlias = getTypeOnlyAliasDeclaration(symbol);
38828+
const isType = !(target.flags & SymbolFlags.Value);
38829+
if (isType || typeOnlyAlias) {
38830+
switch (node.kind) {
38831+
case SyntaxKind.ImportClause:
38832+
case SyntaxKind.ImportSpecifier:
38833+
case SyntaxKind.ImportEqualsDeclaration: {
38834+
if (compilerOptions.preserveValueImports) {
38835+
Debug.assertIsDefined(node.name, "An ImportClause with a symbol should have a name");
38836+
const message = isType
38837+
? Diagnostics._0_is_a_type_and_must_be_imported_using_a_type_only_import_when_preserveValueImports_and_isolatedModules_are_both_enabled
38838+
: Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_imported_using_a_type_only_import_when_preserveValueImports_and_isolatedModules_are_both_enabled;
38839+
const name = idText(node.kind === SyntaxKind.ImportSpecifier ? node.propertyName || node.name : node.name);
38840+
addTypeOnlyDeclarationRelatedInfo(
38841+
error(node, message, name),
38842+
isType ? undefined : typeOnlyAlias,
38843+
name
38844+
);
38845+
}
38846+
break;
38847+
}
38848+
case SyntaxKind.ExportSpecifier: {
38849+
// Don't allow re-exporting an export that will be elided when `--isolatedModules` is set.
38850+
// The exception is that `import type { A } from './a'; export { A }` is allowed
38851+
// because single-file analysis can determine that the export should be dropped.
38852+
if (getSourceFileOfNode(typeOnlyAlias) !== getSourceFileOfNode(node)) {
38853+
const message = isType
38854+
? Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type
38855+
: Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_re_exported_using_a_type_only_re_export_when_isolatedModules_is_enabled;
38856+
const name = idText(node.propertyName || node.name);
38857+
addTypeOnlyDeclarationRelatedInfo(
38858+
error(node, message, name),
38859+
isType ? undefined : typeOnlyAlias,
38860+
name
38861+
);
38862+
return;
38863+
}
38864+
}
38865+
}
38866+
}
3882338867
}
3882438868

3882538869
if (isImportSpecifier(node) && target.declarations?.every(d => !!(getCombinedNodeFlags(d) & NodeFlags.Deprecated))) {
@@ -40600,13 +40644,13 @@ namespace ts {
4060040644
function isValueAliasDeclaration(node: Node): boolean {
4060140645
switch (node.kind) {
4060240646
case SyntaxKind.ImportEqualsDeclaration:
40603-
return isAliasResolvedToValue(getSymbolOfNode(node) || unknownSymbol);
40647+
return isAliasResolvedToValue(getSymbolOfNode(node));
4060440648
case SyntaxKind.ImportClause:
4060540649
case SyntaxKind.NamespaceImport:
4060640650
case SyntaxKind.ImportSpecifier:
4060740651
case SyntaxKind.ExportSpecifier:
40608-
const symbol = getSymbolOfNode(node) || unknownSymbol;
40609-
return isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol);
40652+
const symbol = getSymbolOfNode(node);
40653+
return !!symbol && isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol);
4061040654
case SyntaxKind.ExportDeclaration:
4061140655
const exportClause = (node as ExportDeclaration).exportClause;
4061240656
return !!exportClause && (
@@ -40615,7 +40659,7 @@ namespace ts {
4061540659
);
4061640660
case SyntaxKind.ExportAssignment:
4061740661
return (node as ExportAssignment).expression && (node as ExportAssignment).expression.kind === SyntaxKind.Identifier ?
40618-
isAliasResolvedToValue(getSymbolOfNode(node) || unknownSymbol) :
40662+
isAliasResolvedToValue(getSymbolOfNode(node)) :
4061940663
true;
4062040664
}
4062140665
return false;
@@ -40632,7 +40676,10 @@ namespace ts {
4063240676
return isValue && node.moduleReference && !nodeIsMissing(node.moduleReference);
4063340677
}
4063440678

40635-
function isAliasResolvedToValue(symbol: Symbol): boolean {
40679+
function isAliasResolvedToValue(symbol: Symbol | undefined): boolean {
40680+
if (!symbol) {
40681+
return false;
40682+
}
4063640683
const target = resolveAlias(symbol);
4063740684
if (target === unknownSymbol) {
4063840685
return true;

src/compiler/commandLineParser.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,7 @@ namespace ts {
571571
type: new Map(getEntries({
572572
remove: ImportsNotUsedAsValues.Remove,
573573
preserve: ImportsNotUsedAsValues.Preserve,
574-
error: ImportsNotUsedAsValues.Error
574+
error: ImportsNotUsedAsValues.Error,
575575
})),
576576
affectsEmit: true,
577577
affectsSemanticDiagnostics: true,
@@ -1169,6 +1169,14 @@ namespace ts {
11691169
description: Diagnostics.Emit_ECMAScript_standard_compliant_class_fields,
11701170
defaultValueDescription: Diagnostics.true_for_ES2022_and_above_including_ESNext
11711171
},
1172+
{
1173+
name: "preserveValueImports",
1174+
type: "boolean",
1175+
affectsEmit: true,
1176+
category: Diagnostics.Emit,
1177+
description: Diagnostics.Preserve_unused_imported_values_in_the_JavaScript_output_that_would_otherwise_be_removed,
1178+
},
1179+
11721180
{
11731181
name: "keyofStringsOnly",
11741182
type: "boolean",

src/compiler/diagnosticMessages.json

+20
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,22 @@
13721372
"category": "Error",
13731373
"code": 1443
13741374
},
1375+
"'{0}' is a type and must be imported using a type-only import when 'preserveValueImports' and 'isolatedModules' are both enabled.": {
1376+
"category": "Error",
1377+
"code": 1444
1378+
},
1379+
"'{0}' resolves to a type-only declaration and must be imported using a type-only import when 'preserveValueImports' and 'isolatedModules' are both enabled.": {
1380+
"category": "Error",
1381+
"code": 1446
1382+
},
1383+
"'{0}' resolves to a type-only declaration and must be re-exported using a type-only re-export when 'isolatedModules' is enabled.": {
1384+
"category": "Error",
1385+
"code": 1448
1386+
},
1387+
"Preserve unused imported values in the JavaScript output that would otherwise be removed.": {
1388+
"category": "Message",
1389+
"code": 1449
1390+
},
13751391

13761392
"The types of '{0}' are incompatible between these types.": {
13771393
"category": "Error",
@@ -3910,6 +3926,10 @@
39103926
"category": "Error",
39113927
"code": 5094
39123928
},
3929+
"Option 'preserveValueImports' can only be used when 'module' is set to 'es2015' or later.": {
3930+
"category": "Error",
3931+
"code": 5095
3932+
},
39133933

39143934
"Generates a sourcemap for each corresponding '.d.ts' file.": {
39153935
"category": "Message",

src/compiler/program.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -3305,6 +3305,10 @@ namespace ts {
33053305
}
33063306
}
33073307

3308+
if (options.preserveValueImports && getEmitModuleKind(options) < ModuleKind.ES2015) {
3309+
createOptionValueDiagnostic("importsNotUsedAsValues", Diagnostics.Option_preserveValueImports_can_only_be_used_when_module_is_set_to_es2015_or_later);
3310+
}
3311+
33083312
// If the emit is enabled make sure that every output file is unique and not overwriting any of the input files
33093313
if (!options.noEmit && !options.suppressOutputPathCheck) {
33103314
const emitHost = getEmitHost();
@@ -3575,7 +3579,7 @@ namespace ts {
35753579
createDiagnosticForOption(/*onKey*/ true, option1, option2, message, option1, option2, option3);
35763580
}
35773581

3578-
function createOptionValueDiagnostic(option1: string, message: DiagnosticMessage, arg0: string) {
3582+
function createOptionValueDiagnostic(option1: string, message: DiagnosticMessage, arg0?: string) {
35793583
createDiagnosticForOption(/*onKey*/ false, option1, /*option2*/ undefined, message, arg0);
35803584
}
35813585

@@ -3590,7 +3594,7 @@ namespace ts {
35903594
}
35913595
}
35923596

3593-
function createDiagnosticForOption(onKey: boolean, option1: string, option2: string | undefined, message: DiagnosticMessage, arg0: string | number, arg1?: string | number, arg2?: string | number) {
3597+
function createDiagnosticForOption(onKey: boolean, option1: string, option2: string | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number) {
35943598
const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax();
35953599
const needCompilerDiagnostic = !compilerOptionsObjectLiteralSyntax ||
35963600
!createOptionDiagnosticInObjectLiteralSyntax(compilerOptionsObjectLiteralSyntax, onKey, option1, option2, message, arg0, arg1, arg2);
@@ -3616,7 +3620,7 @@ namespace ts {
36163620
return _compilerOptionsObjectLiteralSyntax || undefined;
36173621
}
36183622

3619-
function createOptionDiagnosticInObjectLiteralSyntax(objectLiteral: ObjectLiteralExpression, onKey: boolean, key1: string, key2: string | undefined, message: DiagnosticMessage, arg0: string | number, arg1?: string | number, arg2?: string | number): boolean {
3623+
function createOptionDiagnosticInObjectLiteralSyntax(objectLiteral: ObjectLiteralExpression, onKey: boolean, key1: string, key2: string | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): boolean {
36203624
const props = getPropertyAssignment(objectLiteral, key1, key2);
36213625
for (const prop of props) {
36223626
programDiagnostics.add(createDiagnosticForNodeInSourceFile(options.configFile!, onKey ? prop.name : prop.initializer, message, arg0, arg1, arg2));

src/compiler/transformers/ts.ts

+17-15
Original file line numberDiff line numberDiff line change
@@ -2791,7 +2791,7 @@ namespace ts {
27912791
}
27922792

27932793
/**
2794-
* Visits an import declaration, eliding it if it is not referenced and `importsNotUsedAsValues` is not 'preserve'.
2794+
* Visits an import declaration, eliding it if it is type-only or if it has an import clause that may be elided.
27952795
*
27962796
* @param node The import declaration node.
27972797
*/
@@ -2821,29 +2821,27 @@ namespace ts {
28212821
}
28222822

28232823
/**
2824-
* Visits an import clause, eliding it if it is not referenced.
2824+
* Visits an import clause, eliding it if its `name` and `namedBindings` may both be elided.
28252825
*
28262826
* @param node The import clause node.
28272827
*/
28282828
function visitImportClause(node: ImportClause): VisitResult<ImportClause> {
2829-
if (node.isTypeOnly) {
2830-
return undefined;
2831-
}
2829+
Debug.assert(!node.isTypeOnly);
28322830
// Elide the import clause if we elide both its name and its named bindings.
2833-
const name = resolver.isReferencedAliasDeclaration(node) ? node.name : undefined;
2831+
const name = shouldEmitAliasDeclaration(node) ? node.name : undefined;
28342832
const namedBindings = visitNode(node.namedBindings, visitNamedImportBindings, isNamedImportBindings);
28352833
return (name || namedBindings) ? factory.updateImportClause(node, /*isTypeOnly*/ false, name, namedBindings) : undefined;
28362834
}
28372835

28382836
/**
2839-
* Visits named import bindings, eliding it if it is not referenced.
2837+
* Visits named import bindings, eliding them if their targets, their references, and the compilation settings allow.
28402838
*
28412839
* @param node The named import bindings node.
28422840
*/
28432841
function visitNamedImportBindings(node: NamedImportBindings): VisitResult<NamedImportBindings> {
28442842
if (node.kind === SyntaxKind.NamespaceImport) {
28452843
// Elide a namespace import if it is not referenced.
2846-
return resolver.isReferencedAliasDeclaration(node) ? node : undefined;
2844+
return shouldEmitAliasDeclaration(node) ? node : undefined;
28472845
}
28482846
else {
28492847
// Elide named imports if all of its import specifiers are elided.
@@ -2853,13 +2851,12 @@ namespace ts {
28532851
}
28542852

28552853
/**
2856-
* Visits an import specifier, eliding it if it is not referenced.
2854+
* Visits an import specifier, eliding it if its target, its references, and the compilation settings allow.
28572855
*
28582856
* @param node The import specifier node.
28592857
*/
28602858
function visitImportSpecifier(node: ImportSpecifier): VisitResult<ImportSpecifier> {
2861-
// Elide an import specifier if it is not referenced.
2862-
return resolver.isReferencedAliasDeclaration(node) ? node : undefined;
2859+
return shouldEmitAliasDeclaration(node) ? node : undefined;
28632860
}
28642861

28652862
/**
@@ -2876,8 +2873,7 @@ namespace ts {
28762873
}
28772874

28782875
/**
2879-
* Visits an export declaration, eliding it if it does not contain a clause that resolves
2880-
* to a value.
2876+
* Visits an export declaration, eliding it if it does not contain a clause that resolves to a value.
28812877
*
28822878
* @param node The export declaration node.
28832879
*/
@@ -2950,7 +2946,7 @@ namespace ts {
29502946
// preserve old compiler's behavior: emit 'var' for import declaration (even if we do not consider them referenced) when
29512947
// - current file is not external module
29522948
// - import declaration is top level and target is value imported by entity name
2953-
return resolver.isReferencedAliasDeclaration(node)
2949+
return shouldEmitAliasDeclaration(node)
29542950
|| (!isExternalModule(currentSourceFile)
29552951
&& resolver.isTopLevelValueImportEqualsWithEntityName(node));
29562952
}
@@ -2967,7 +2963,7 @@ namespace ts {
29672963
}
29682964

29692965
if (isExternalModuleImportEqualsDeclaration(node)) {
2970-
const isReferenced = resolver.isReferencedAliasDeclaration(node);
2966+
const isReferenced = shouldEmitAliasDeclaration(node);
29712967
// If the alias is unreferenced but we want to keep the import, replace with 'import "mod"'.
29722968
if (!isReferenced && compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve) {
29732969
return setOriginalNode(
@@ -3358,5 +3354,11 @@ namespace ts {
33583354

33593355
return isPropertyAccessExpression(node) || isElementAccessExpression(node) ? resolver.getConstantValue(node) : undefined;
33603356
}
3357+
3358+
function shouldEmitAliasDeclaration(node: Node): boolean {
3359+
return compilerOptions.preserveValueImports
3360+
? resolver.isValueAliasDeclaration(node)
3361+
: resolver.isReferencedAliasDeclaration(node);
3362+
}
33613363
}
33623364
}

src/compiler/types.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -3109,6 +3109,14 @@ namespace ts {
31093109
| ImportOrExportSpecifier
31103110
;
31113111

3112+
export type TypeOnlyAliasDeclaration =
3113+
| ImportClause & { readonly isTypeOnly: true, readonly name: Identifier }
3114+
| ImportEqualsDeclaration & { readonly isTypeOnly: true }
3115+
| NamespaceImport & { readonly parent: ImportClause & { readonly isTypeOnly: true } }
3116+
| ImportSpecifier & { readonly parent: NamedImports & { readonly parent: ImportClause & { readonly isTypeOnly: true } } }
3117+
| ExportSpecifier & { readonly parent: NamedExports & { readonly parent: ExportDeclaration & { readonly isTypeOnly: true } } }
3118+
;
3119+
31123120
/**
31133121
* This is either an `export =` or an `export default` declaration.
31143122
* Unless `isExportEquals` is set, this node was parsed as an `export default`.
@@ -6057,6 +6065,7 @@ namespace ts {
60576065
preserveConstEnums?: boolean;
60586066
noImplicitOverride?: boolean;
60596067
preserveSymlinks?: boolean;
6068+
preserveValueImports?: boolean;
60606069
/* @internal */ preserveWatchOutput?: boolean;
60616070
project?: string;
60626071
/* @internal */ pretty?: boolean;
@@ -6150,7 +6159,7 @@ namespace ts {
61506159
export const enum ImportsNotUsedAsValues {
61516160
Remove,
61526161
Preserve,
6153-
Error
6162+
Error,
61546163
}
61556164

61566165
export const enum NewLineKind {

src/compiler/utilitiesPublic.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,7 @@ namespace ts {
11261126
return isImportSpecifier(node) || isExportSpecifier(node);
11271127
}
11281128

1129-
export function isTypeOnlyImportOrExportDeclaration(node: Node): node is TypeOnlyCompatibleAliasDeclaration {
1129+
export function isTypeOnlyImportOrExportDeclaration(node: Node): node is TypeOnlyAliasDeclaration {
11301130
switch (node.kind) {
11311131
case SyntaxKind.ImportSpecifier:
11321132
case SyntaxKind.ExportSpecifier:

0 commit comments

Comments
 (0)