Skip to content

Add --importsNotUsedAsValues=preserve-exact #44137

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

Closed
Closed
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
76 changes: 64 additions & 12 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2166,17 +2166,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 @@ -38072,13 +38080,57 @@ 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)
&& !(node.flags & NodeFlags.Ambient)) {
error(node, Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type);
const isDeclaredTypeOnly = isTypeOnlyImportOrExportDeclaration(node);
if ((compilerOptions.isolatedModules || compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.PreserveExact)
&& !isDeclaredTypeOnly
&& !(node.flags & NodeFlags.Ambient)
&& (!(target.flags & SymbolFlags.Value) || getTypeOnlyAliasDeclaration(symbol))) {
const isType = !(target.flags & SymbolFlags.Value);
const typeOnlyAlias = getTypeOnlyAliasDeclaration(symbol);

switch (node.kind) {
case SyntaxKind.ImportClause:
case SyntaxKind.ImportSpecifier:
case SyntaxKind.ImportEqualsDeclaration: {
if (compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.PreserveExact) {
const message = isType
? Diagnostics._0_is_a_type_and_must_be_imported_with_a_type_only_import_when_importsNotUsedAsValues_is_set_to_preserve_exact
: Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_imported_with_a_type_only_import_when_importsNotUsedAsValues_is_set_to_preserve_exact;
const name = idText(node.kind === SyntaxKind.ImportSpecifier ? node.propertyName || node.name : node.name!);
addTypeOnlyDeclarationRelatedInfo(
error(node, message, name),
isType ? undefined : typeOnlyAlias,
name
);
}
break;
}
case SyntaxKind.ExportSpecifier: {
// Don't allow re-exporting an export that will be elided when `--isolatedModules` is set
// or when `--importsNotUsedAsValues` is `preserve-exact`.
if (compilerOptions.isolatedModules &&
compilerOptions.importsNotUsedAsValues !== ImportsNotUsedAsValues.PreserveExact &&
typeOnlyAlias && getSourceFileOfNode(typeOnlyAlias) === getSourceFileOfNode(node)) {
// In `isolatedModules` alone, `import type { A } from './a'; export { A }` is allowed
// because single-file analysis can determine that the export should be dropped.
// `--importsNotUsedAsValues=preserv-exact` is stricter; it refuses to touch non-type-only
// imports and exports, so `export { A }` is not allowed if 'A' is not emitted.
return;
}
const message =
compilerOptions.isolatedModules && isType ? Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type :
compilerOptions.isolatedModules && !isType ? Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_re_exported_with_a_type_only_re_export_when_isolatedModules_is_enabled :
isType ? Diagnostics._0_is_a_type_and_must_be_re_exported_with_a_type_only_re_export_when_importsNotUsedAsValues_is_set_to_preserve_exact :
Diagnostics._0_resolves_to_a_type_only_declaration_and_must_be_re_exported_with_a_type_only_re_export_when_importsNotUsedAsValues_is_set_to_preserve_exact;
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
7 changes: 4 additions & 3 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,10 @@ namespace ts {
{
name: "importsNotUsedAsValues",
type: new Map(getEntries({
remove: ImportsNotUsedAsValues.Remove,
preserve: ImportsNotUsedAsValues.Preserve,
error: ImportsNotUsedAsValues.Error
"remove": ImportsNotUsedAsValues.Remove,
"preserve": ImportsNotUsedAsValues.Preserve,
"error": ImportsNotUsedAsValues.Error,
"preserve-exact": ImportsNotUsedAsValues.PreserveExact,
})),
affectsEmit: true,
affectsSemanticDiagnostics: true,
Expand Down
24 changes: 24 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,26 @@
"category": "Error",
"code": 1433
},
"'{0}' is a type and must be imported with a type-only import when 'importsNotUsedAsValues' is set to 'preserve-exact'.": {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The predecessors to these messages try to say 'import type' and 'export type' instead of type-only import and type-only export, but these were worded with an eye toward allowing type-only import and export specifiers, as mentioned in the PR description. If we decide not to do that, I would reword these to say 'import type' and 'export type' since there would only be one syntax that can fix the error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the imported thing with this error could technically be an uninstantiated namespace, so “is a type” might be technically incorrect, but I think I prefer calling an uninstantiated namespaces a “type” to calling it an “uninstantiated namespace” in user-facing messages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • is a type
  • is a type-only entity
  • is only used for type constructs
  • is only used for type-checking
  • is a typey boi

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"has no runtime representation"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, more specifically:

"'{0}' is a type or namespace that does not have an associated runtime value. It must be declared as a type-only import when 'importsNotUsedAsValues' is set to 'preserve-exact'."

"category": "Error",
"code": 1434
},
"'{0}' is a type and must be re-exported with a type-only re-export when 'importsNotUsedAsValues' is set to 'preserve-exact'.": {
"category": "Error",
"code": 1435
},
"'{0}' resolves to a type-only declaration and must be imported with a type-only import when 'importsNotUsedAsValues' is set to 'preserve-exact'.": {
"category": "Error",
"code": 1436
},
"'{0}' resolves to a type-only declaration and must be re-exported with a type-only re-export when 'importsNotUsedAsValues' is set to 'preserve-exact'.": {
"category": "Error",
"code": 1437
},
"'{0}' resolves to a type-only declaration and must be re-exported with a type-only re-export when 'isolatedModules' is enabled.": {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of this diagnostic for isolatedModules is actually a bug fix which should have been included in type-only import work from the beginning.

"category": "Error",
"code": 1438
},

"The types of '{0}' are incompatible between these types.": {
"category": "Error",
Expand Down Expand Up @@ -3942,6 +3962,10 @@
"category": "Error",
"code": 5094
},
"Option 'importsNotUsedAsValues' may be set to 'preserve-exact' only 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 @@ -3295,6 +3295,10 @@ namespace ts {
}
}

if (options.importsNotUsedAsValues === ImportsNotUsedAsValues.PreserveExact && getEmitModuleKind(options) < ModuleKind.ES2015) {
createOptionValueDiagnostic("importsNotUsedAsValues", Diagnostics.Option_importsNotUsedAsValues_may_be_set_to_preserve_exact_only_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 @@ -3565,7 +3569,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 @@ -3580,7 +3584,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 @@ -3606,7 +3610,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
21 changes: 12 additions & 9 deletions src/compiler/transformers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ namespace ts {
const strictNullChecks = getStrictOptionValue(compilerOptions, "strictNullChecks");
const languageVersion = getEmitScriptTarget(compilerOptions);
const moduleKind = getEmitModuleKind(compilerOptions);
const { importsNotUsedAsValues } = compilerOptions;

// Save the previous transformation hooks.
const previousOnEmitNode = context.onEmitNode;
Expand Down Expand Up @@ -2802,7 +2803,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 not referenced and `importsNotUsedAsValues` is not 'preserve' or 'preserve-exact'.
*
* @param node The import declaration node.
*/
Expand All @@ -2820,8 +2821,9 @@ namespace ts {
// Elide the declaration if the import clause was elided.
const importClause = visitNode(node.importClause, visitImportClause, isImportClause);
return importClause ||
compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve ||
compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error
importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve ||
importsNotUsedAsValues === ImportsNotUsedAsValues.PreserveExact ||
importsNotUsedAsValues === ImportsNotUsedAsValues.Error
? factory.updateImportDeclaration(
node,
/*decorators*/ undefined,
Expand All @@ -2832,13 +2834,14 @@ namespace ts {
}

/**
* Visits an import clause, eliding it if it is not referenced.
* Visits an import clause, eliding it if it is not referenced and `importsNotUsedAsValues` is not 'preserve-exact'.
*
* @param node The import clause node.
*/
function visitImportClause(node: ImportClause): VisitResult<ImportClause> {
if (node.isTypeOnly) {
return undefined;
Debug.assert(!node.isTypeOnly);
if (importsNotUsedAsValues === ImportsNotUsedAsValues.PreserveExact) {
return node;
}
// Elide the import clause if we elide both its name and its named bindings.
const name = resolver.isReferencedAliasDeclaration(node) ? node.name : undefined;
Expand Down Expand Up @@ -2888,7 +2891,7 @@ namespace ts {

/**
* Visits an export declaration, eliding it if it does not contain a clause that resolves
* to a value.
* to a value and if `importsNotUsedAsValues` is not 'preserve-exact'.
*
* @param node The export declaration node.
*/
Expand All @@ -2897,7 +2900,7 @@ namespace ts {
return undefined;
}

if (!node.exportClause || isNamespaceExport(node.exportClause)) {
if (!node.exportClause || isNamespaceExport(node.exportClause) || importsNotUsedAsValues === ImportsNotUsedAsValues.PreserveExact) {
// never elide `export <whatever> from <whereever>` declarations -
// they should be kept for sideffects/untyped exports, even when the
// type checker doesn't know about any exports
Expand Down Expand Up @@ -2980,7 +2983,7 @@ namespace ts {
if (isExternalModuleImportEqualsDeclaration(node)) {
const isReferenced = resolver.isReferencedAliasDeclaration(node);
// If the alias is unreferenced but we want to keep the import, replace with 'import "mod"'.
if (!isReferenced && compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve) {
if (!isReferenced && importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve) {
return setOriginalNode(
setTextRange(
factory.createImportDeclaration(
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6070,7 +6070,8 @@ namespace ts {
export const enum ImportsNotUsedAsValues {
Remove,
Preserve,
Error
Error,
PreserveExact,
}

export const enum NewLineKind {
Expand Down
3 changes: 2 additions & 1 deletion tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2946,7 +2946,8 @@ declare namespace ts {
export enum ImportsNotUsedAsValues {
Remove = 0,
Preserve = 1,
Error = 2
Error = 2,
PreserveExact = 3
}
export enum NewLineKind {
CarriageReturnLineFeed = 0,
Expand Down
3 changes: 2 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2946,7 +2946,8 @@ declare namespace ts {
export enum ImportsNotUsedAsValues {
Remove = 0,
Preserve = 1,
Error = 2
Error = 2,
PreserveExact = 3
}
export enum NewLineKind {
CarriageReturnLineFeed = 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
tests/cases/conformance/externalModules/typeOnly/d.ts(1,1): error TS1203: Export assignment cannot be used when targeting ECMAScript modules. Consider using 'export default' or another module format instead.
tests/cases/conformance/externalModules/typeOnly/e.ts(1,1): error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.
tests/cases/conformance/externalModules/typeOnly/e.ts(2,1): error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.


==== tests/cases/conformance/externalModules/typeOnly/a.ts (0 errors) ====
export default {};
export const b = 0;
export const c = 1;

==== tests/cases/conformance/externalModules/typeOnly/b.ts (0 errors) ====
import a, { b, c } from "./a";

==== tests/cases/conformance/externalModules/typeOnly/c.ts (0 errors) ====
import * as a from "./a";

==== tests/cases/conformance/externalModules/typeOnly/d.ts (1 errors) ====
export = {};
~~~~~~~~~~~~
!!! error TS1203: Export assignment cannot be used when targeting ECMAScript modules. Consider using 'export default' or another module format instead.

==== tests/cases/conformance/externalModules/typeOnly/e.ts (2 errors) ====
import D = require("./d");
~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.
import DD = require("./d");
~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.
DD;

==== tests/cases/conformance/externalModules/typeOnly/f.ts (0 errors) ====
import type a from "./a";
import { b, c } from "./a";
b;

43 changes: 43 additions & 0 deletions tests/baselines/reference/importsNotUsedAsValues_preserve-exact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//// [tests/cases/conformance/externalModules/typeOnly/importsNotUsedAsValues_preserve-exact.ts] ////

//// [a.ts]
export default {};
export const b = 0;
export const c = 1;

//// [b.ts]
import a, { b, c } from "./a";

//// [c.ts]
import * as a from "./a";

//// [d.ts]
export = {};

//// [e.ts]
import D = require("./d");
import DD = require("./d");
DD;

//// [f.ts]
import type a from "./a";
import { b, c } from "./a";
b;


//// [a.js]
export default {};
export var b = 0;
export var c = 1;
//// [b.js]
import a, { b, c } from "./a";
//// [c.js]
import * as a from "./a";
//// [d.js]
export {};
//// [e.js]
DD;
export {};
//// [f.js]
import { b, c } from "./a";
b;
Loading