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

Add --preserveValueImports #44619

Merged
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
77 changes: 62 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2191,17 +2191,25 @@ namespace ts {
const message = isExport
? Diagnostics._0_cannot_be_used_as_a_value_because_it_was_exported_using_export_type
: Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type;
const relatedMessage = isExport
? Diagnostics._0_was_exported_here
: Diagnostics._0_was_imported_here;
const unescapedName = unescapeLeadingUnderscores(name);
addRelatedInfo(
addTypeOnlyDeclarationRelatedInfo(
error(useSite, message, unescapedName),
createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, unescapedName));
typeOnlyDeclaration,
unescapedName);
}
}
}

function addTypeOnlyDeclarationRelatedInfo(diagnostic: Diagnostic, typeOnlyDeclaration: TypeOnlyCompatibleAliasDeclaration | undefined, unescapedName: string) {
if (!typeOnlyDeclaration) return diagnostic;
return addRelatedInfo(
diagnostic,
createDiagnosticForNode(
typeOnlyDeclaration,
typeOnlyDeclarationIsExport(typeOnlyDeclaration) ? Diagnostics._0_was_exported_here : Diagnostics._0_was_imported_here,
unescapedName));
}

function getIsDeferredContext(location: Node, lastLocation: Node | undefined): boolean {
if (location.kind !== SyntaxKind.ArrowFunction && location.kind !== SyntaxKind.FunctionExpression) {
// initializers in instance property declaration of class like entities are executed in constructor and thus deferred
Expand Down Expand Up @@ -38599,13 +38607,49 @@ namespace ts {
error(node, message, symbolToString(symbol));
}

// Don't allow to re-export something with no value side when `--isolatedModules` is set.
if (compilerOptions.isolatedModules
&& node.kind === SyntaxKind.ExportSpecifier
&& !node.parent.parent.isTypeOnly
&& !(target.flags & SymbolFlags.Value)
&& !isTypeOnlyImportOrExportDeclaration(node)
&& !(node.flags & NodeFlags.Ambient)) {
error(node, Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type);
const typeOnlyAlias = getTypeOnlyAliasDeclaration(symbol);
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
const isType = !(target.flags & SymbolFlags.Value);
if (isType || typeOnlyAlias) {
switch (node.kind) {
case SyntaxKind.ImportClause:
case SyntaxKind.ImportSpecifier:
case SyntaxKind.ImportEqualsDeclaration: {
if (compilerOptions.preserveValueImports) {
Debug.assertIsDefined(node.name, "An ImportClause with a symbol should have a name");
const message = isType
? Diagnostics._0_is_a_type_and_must_be_imported_using_a_type_only_import_when_preserveValueImports_and_isolatedModules_are_both_enabled
: Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_imported_using_a_type_only_import_when_preserveValueImports_and_isolatedModules_are_both_enabled;
const name = idText(node.kind === SyntaxKind.ImportSpecifier ? node.propertyName || node.name : node.name);
addTypeOnlyDeclarationRelatedInfo(
error(node, message, name),
isType ? undefined : typeOnlyAlias,
name
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
);
}
break;
}
case SyntaxKind.ExportSpecifier: {
// Don't allow re-exporting an export that will be elided when `--isolatedModules` is set.
// The exception is that `import type { A } from './a'; export { A }` is allowed
// because single-file analysis can determine that the export should be dropped.
if (getSourceFileOfNode(typeOnlyAlias) !== getSourceFileOfNode(node)) {
const message = isType
? Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type
: Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_re_exported_using_a_type_only_re_export_when_isolatedModules_is_enabled;
const name = idText(node.propertyName || node.name);
addTypeOnlyDeclarationRelatedInfo(
error(node, message, name),
isType ? undefined : typeOnlyAlias,
name
);
return;
}
}
}
}
}

if (isImportSpecifier(node) && target.declarations?.every(d => !!(getCombinedNodeFlags(d) & NodeFlags.Deprecated))) {
Expand Down Expand Up @@ -40386,13 +40430,13 @@ namespace ts {
function isValueAliasDeclaration(node: Node): boolean {
switch (node.kind) {
case SyntaxKind.ImportEqualsDeclaration:
return isAliasResolvedToValue(getSymbolOfNode(node) || unknownSymbol);
return isAliasResolvedToValue(getSymbolOfNode(node));
case SyntaxKind.ImportClause:
case SyntaxKind.NamespaceImport:
case SyntaxKind.ImportSpecifier:
case SyntaxKind.ExportSpecifier:
const symbol = getSymbolOfNode(node) || unknownSymbol;
return isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol);
const symbol = getSymbolOfNode(node);
return !!symbol && isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol);
case SyntaxKind.ExportDeclaration:
const exportClause = (node as ExportDeclaration).exportClause;
return !!exportClause && (
Expand All @@ -40401,7 +40445,7 @@ namespace ts {
);
case SyntaxKind.ExportAssignment:
return (node as ExportAssignment).expression && (node as ExportAssignment).expression.kind === SyntaxKind.Identifier ?
isAliasResolvedToValue(getSymbolOfNode(node) || unknownSymbol) :
isAliasResolvedToValue(getSymbolOfNode(node)) :
true;
}
return false;
Expand All @@ -40418,7 +40462,10 @@ namespace ts {
return isValue && node.moduleReference && !nodeIsMissing(node.moduleReference);
}

function isAliasResolvedToValue(symbol: Symbol): boolean {
function isAliasResolvedToValue(symbol: Symbol | undefined): boolean {
if (!symbol) {
return false;
}
const target = resolveAlias(symbol);
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
if (target === unknownSymbol) {
return true;
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ namespace ts {
type: new Map(getEntries({
remove: ImportsNotUsedAsValues.Remove,
preserve: ImportsNotUsedAsValues.Preserve,
error: ImportsNotUsedAsValues.Error
error: ImportsNotUsedAsValues.Error,
})),
affectsEmit: true,
affectsSemanticDiagnostics: true,
Expand Down Expand Up @@ -1168,6 +1168,14 @@ namespace ts {
description: Diagnostics.Emit_ECMAScript_standard_compliant_class_fields,
defaultValueDescription: "false"
},
{
name: "preserveValueImports",
type: "boolean",
affectsEmit: true,
category: Diagnostics.Emit,
description: Diagnostics.Preserve_unused_imported_values_in_the_JavaScript_output_that_would_otherwise_be_removed,
},

{
name: "keyofStringsOnly",
type: "boolean",
Expand Down
20 changes: 20 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,22 @@
"category": "Error",
"code": 1443
},
"'{0}' is a type and must be imported using a type-only import when 'preserveValueImports' and 'isolatedModules' are both enabled.": {
"category": "Error",
"code": 1444
},
"'{0}' resolves to a type-only declaration and must be imported using a type-only import when 'preserveValueImports' and 'isolatedModules' are both enabled.": {
"category": "Error",
"code": 1446
},
"'{0}' resolves to a type-only declaration and must be re-exported using a type-only re-export when 'isolatedModules' is enabled.": {
"category": "Error",
"code": 1448
},
"Preserve unused imported values in the JavaScript output that would otherwise be removed.": {
"category": "Message",
"code": 1449
},

"The types of '{0}' are incompatible between these types.": {
"category": "Error",
Expand Down Expand Up @@ -4038,6 +4054,10 @@
"category": "Error",
"code": 5094
},
"Option 'preserveValueImports' can only be used when 'module' is set to 'es2015' or later.": {
"category": "Error",
"code": 5095
},

"Generates a sourcemap for each corresponding '.d.ts' file.": {
"category": "Message",
Expand Down
10 changes: 7 additions & 3 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3307,6 +3307,10 @@ namespace ts {
}
}

if (options.preserveValueImports && getEmitModuleKind(options) < ModuleKind.ES2015) {
createOptionValueDiagnostic("importsNotUsedAsValues", Diagnostics.Option_preserveValueImports_can_only_be_used_when_module_is_set_to_es2015_or_later);
}

// If the emit is enabled make sure that every output file is unique and not overwriting any of the input files
if (!options.noEmit && !options.suppressOutputPathCheck) {
const emitHost = getEmitHost();
Expand Down Expand Up @@ -3577,7 +3581,7 @@ namespace ts {
createDiagnosticForOption(/*onKey*/ true, option1, option2, message, option1, option2, option3);
}

function createOptionValueDiagnostic(option1: string, message: DiagnosticMessage, arg0: string) {
function createOptionValueDiagnostic(option1: string, message: DiagnosticMessage, arg0?: string) {
createDiagnosticForOption(/*onKey*/ false, option1, /*option2*/ undefined, message, arg0);
}

Expand All @@ -3592,7 +3596,7 @@ namespace ts {
}
}

function createDiagnosticForOption(onKey: boolean, option1: string, option2: string | undefined, message: DiagnosticMessage, arg0: string | number, arg1?: string | number, arg2?: string | number) {
function createDiagnosticForOption(onKey: boolean, option1: string, option2: string | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number) {
const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax();
const needCompilerDiagnostic = !compilerOptionsObjectLiteralSyntax ||
!createOptionDiagnosticInObjectLiteralSyntax(compilerOptionsObjectLiteralSyntax, onKey, option1, option2, message, arg0, arg1, arg2);
Expand All @@ -3618,7 +3622,7 @@ namespace ts {
return _compilerOptionsObjectLiteralSyntax || undefined;
}

function createOptionDiagnosticInObjectLiteralSyntax(objectLiteral: ObjectLiteralExpression, onKey: boolean, key1: string, key2: string | undefined, message: DiagnosticMessage, arg0: string | number, arg1?: string | number, arg2?: string | number): boolean {
function createOptionDiagnosticInObjectLiteralSyntax(objectLiteral: ObjectLiteralExpression, onKey: boolean, key1: string, key2: string | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): boolean {
const props = getPropertyAssignment(objectLiteral, key1, key2);
for (const prop of props) {
programDiagnostics.add(createDiagnosticForNodeInSourceFile(options.configFile!, onKey ? prop.name : prop.initializer, message, arg0, arg1, arg2));
Expand Down
32 changes: 17 additions & 15 deletions src/compiler/transformers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2791,7 +2791,7 @@ namespace ts {
}

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

/**
* Visits an import clause, eliding it if it is not referenced.
* Visits an import clause, eliding it if its `name` and `namedBindings` may both be elided.
*
* @param node The import clause node.
*/
function visitImportClause(node: ImportClause): VisitResult<ImportClause> {
if (node.isTypeOnly) {
return undefined;
}
Debug.assert(!node.isTypeOnly);
// Elide the import clause if we elide both its name and its named bindings.
const name = resolver.isReferencedAliasDeclaration(node) ? node.name : undefined;
const name = shouldEmitAliasDeclaration(node) ? node.name : undefined;
const namedBindings = visitNode(node.namedBindings, visitNamedImportBindings, isNamedImportBindings);
return (name || namedBindings) ? factory.updateImportClause(node, /*isTypeOnly*/ false, name, namedBindings) : undefined;
}

/**
* Visits named import bindings, eliding it if it is not referenced.
* Visits named import bindings, eliding them if their targets, their references, and the compilation settings allow.
*
* @param node The named import bindings node.
*/
function visitNamedImportBindings(node: NamedImportBindings): VisitResult<NamedImportBindings> {
if (node.kind === SyntaxKind.NamespaceImport) {
// Elide a namespace import if it is not referenced.
return resolver.isReferencedAliasDeclaration(node) ? node : undefined;
return shouldEmitAliasDeclaration(node) ? node : undefined;
}
else {
// Elide named imports if all of its import specifiers are elided.
Expand All @@ -2853,13 +2851,12 @@ namespace ts {
}

/**
* Visits an import specifier, eliding it if it is not referenced.
* Visits an import specifier, eliding it if its target, its references, and the compilation settings allow.
*
* @param node The import specifier node.
*/
function visitImportSpecifier(node: ImportSpecifier): VisitResult<ImportSpecifier> {
// Elide an import specifier if it is not referenced.
return resolver.isReferencedAliasDeclaration(node) ? node : undefined;
return shouldEmitAliasDeclaration(node) ? node : undefined;
}

/**
Expand All @@ -2876,8 +2873,7 @@ namespace ts {
}

/**
* Visits an export declaration, eliding it if it does not contain a clause that resolves
* to a value.
* Visits an export declaration, eliding it if it does not contain a clause that resolves to a value.
*
* @param node The export declaration node.
*/
Expand Down Expand Up @@ -2950,7 +2946,7 @@ namespace ts {
// preserve old compiler's behavior: emit 'var' for import declaration (even if we do not consider them referenced) when
// - current file is not external module
// - import declaration is top level and target is value imported by entity name
return resolver.isReferencedAliasDeclaration(node)
return shouldEmitAliasDeclaration(node)
|| (!isExternalModule(currentSourceFile)
&& resolver.isTopLevelValueImportEqualsWithEntityName(node));
}
Expand All @@ -2967,7 +2963,7 @@ namespace ts {
}

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

return isPropertyAccessExpression(node) || isElementAccessExpression(node) ? resolver.getConstantValue(node) : undefined;
}

function shouldEmitAliasDeclaration(node: Node): boolean {
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
return compilerOptions.preserveValueImports
? resolver.isValueAliasDeclaration(node)
: resolver.isReferencedAliasDeclaration(node);
}
}
}
11 changes: 10 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3104,6 +3104,14 @@ namespace ts {
| ImportOrExportSpecifier
;

export type TypeOnlyAliasDeclaration =
| ImportClause & { readonly isTypeOnly: true, readonly name: Identifier }
| ImportEqualsDeclaration & { readonly isTypeOnly: true }
| NamespaceImport & { readonly parent: ImportClause & { readonly isTypeOnly: true } }
| ImportSpecifier & { readonly parent: NamedImports & { readonly parent: ImportClause & { readonly isTypeOnly: true } } }
| ExportSpecifier & { readonly parent: NamedExports & { readonly parent: ExportDeclaration & { readonly isTypeOnly: true } } }
;

/**
* This is either an `export =` or an `export default` declaration.
* Unless `isExportEquals` is set, this node was parsed as an `export default`.
Expand Down Expand Up @@ -6049,6 +6057,7 @@ namespace ts {
preserveConstEnums?: boolean;
noImplicitOverride?: boolean;
preserveSymlinks?: boolean;
preserveValueImports?: boolean;
/* @internal */ preserveWatchOutput?: boolean;
project?: string;
/* @internal */ pretty?: boolean;
Expand Down Expand Up @@ -6142,7 +6151,7 @@ namespace ts {
export const enum ImportsNotUsedAsValues {
Remove,
Preserve,
Error
Error,
}

export const enum NewLineKind {
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/utilitiesPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,7 @@ namespace ts {
return isImportSpecifier(node) || isExportSpecifier(node);
}

export function isTypeOnlyImportOrExportDeclaration(node: Node): node is TypeOnlyCompatibleAliasDeclaration {
export function isTypeOnlyImportOrExportDeclaration(node: Node): node is TypeOnlyAliasDeclaration {
switch (node.kind) {
case SyntaxKind.ImportSpecifier:
case SyntaxKind.ExportSpecifier:
Expand Down
Loading