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

isolatedModules errors for non-literal enum initializers #56736

Merged
merged 11 commits into from Mar 20, 2024
64 changes: 58 additions & 6 deletions src/compiler/checker.ts
Expand Up @@ -45010,15 +45010,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!(nodeLinks.flags & NodeCheckFlags.EnumValuesComputed)) {
nodeLinks.flags |= NodeCheckFlags.EnumValuesComputed;
let autoValue: number | undefined = 0;
let previous: EnumMember | undefined;
for (const member of node.members) {
const value = computeMemberValue(member, autoValue);
const value = computeMemberValue(member, autoValue, previous);
getNodeLinks(member).enumMemberValue = value;
autoValue = typeof value === "number" ? value + 1 : undefined;
previous = member;
}
}
}

function computeMemberValue(member: EnumMember, autoValue: number | undefined) {
function computeMemberValue(member: EnumMember, autoValue: number | undefined, previous: EnumMember | undefined) {
if (isComputedNonLiteralName(member.name)) {
error(member.name, Diagnostics.Computed_property_names_are_not_allowed_in_enums);
}
Expand All @@ -45040,11 +45042,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// If the member is the first member in the enum declaration, it is assigned the value zero.
// Otherwise, it is assigned the value of the immediately preceding member plus one, and an error
// occurs if the immediately preceding member is not a constant enum member.
if (autoValue !== undefined) {
return autoValue;
if (autoValue === undefined) {
error(member.name, Diagnostics.Enum_member_must_have_initializer);
return undefined;
}
error(member.name, Diagnostics.Enum_member_must_have_initializer);
return undefined;
if (compilerOptions.isolatedModules && previous?.initializer && !evaluatesToNumericLiteral(previous.initializer)) {
frigus02 marked this conversation as resolved.
Show resolved Hide resolved
error(
member.name,
Diagnostics.Enum_member_following_a_non_literal_numeric_member_must_have_an_initializer_when_isolatedModules_is_enabled,
);
}
return autoValue;
}

function computeConstantValue(member: EnumMember): string | number | undefined {
Expand All @@ -45060,6 +45068,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value,
);
}
else if (compilerOptions.isolatedModules && typeof value === "string" && !evaluatesToStringLiteral(initializer)) {
error(
initializer,
Diagnostics.A_member_initializer_in_a_enum_declaration_for_a_string_value_must_be_a_string_literal_when_isolatedModules_is_enabled,
);
}
}
else if (isConstEnum) {
error(initializer, Diagnostics.const_enum_member_initializers_must_be_constant_expressions);
Expand All @@ -45073,6 +45087,44 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return value;
}

function evaluatesToNumericLiteral(expr: Expression): boolean {
frigus02 marked this conversation as resolved.
Show resolved Hide resolved
switch (expr.kind) {
case SyntaxKind.PrefixUnaryExpression:
return evaluatesToNumericLiteral((expr as PrefixUnaryExpression).operand);
case SyntaxKind.BinaryExpression:
return evaluatesToNumericLiteral((expr as BinaryExpression).left) && evaluatesToNumericLiteral((expr as BinaryExpression).right);
case SyntaxKind.ParenthesizedExpression:
return evaluatesToNumericLiteral((expr as ParenthesizedExpression).expression);
case SyntaxKind.NumericLiteral:
return true;
}
return false;
}

function evaluatesToStringLiteral(expr: Expression): boolean {
switch (expr.kind) {
case SyntaxKind.BinaryExpression:
const left = (expr as BinaryExpression).left;
const right = (expr as BinaryExpression).right;
const leftIsNumeric = evaluatesToNumericLiteral(left);
const rightIsNumeric = evaluatesToNumericLiteral(right);
return (
!(leftIsNumeric && rightIsNumeric) &&
(evaluatesToStringLiteral(left) || leftIsNumeric) &&
(evaluatesToStringLiteral(right) || rightIsNumeric) &&
(expr as BinaryExpression).operatorToken.kind === SyntaxKind.PlusToken
);
case SyntaxKind.TemplateExpression:
return (expr as TemplateExpression).templateSpans.every(span => evaluatesToStringLiteral(span.expression));
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
case SyntaxKind.ParenthesizedExpression:
return evaluatesToStringLiteral((expr as ParenthesizedExpression).expression);
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
return true;
}
return false;
}

function evaluate(expr: Expression, location?: Declaration): string | number | undefined {
switch (expr.kind) {
case SyntaxKind.PrefixUnaryExpression:
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Expand Up @@ -7936,5 +7936,13 @@
"'await using' statements cannot be used inside a class static block.": {
"category": "Error",
"code": 18054
},
"A member initializer in a enum declaration for a string value must be a string literal when 'isolatedModules' is enabled.": {
frigus02 marked this conversation as resolved.
Show resolved Hide resolved
"category": "Error",
"code": 18055
},
"Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.": {
"category": "Error",
"code": 18056
}
}
@@ -0,0 +1,34 @@
bad.ts(4,5): error TS18056: Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.


==== ./helpers.ts (0 errors) ====
export const foo = 2;

==== ./bad.ts (1 errors) ====
import { foo } from "./helpers";
enum A {
a = foo,
b,
~
!!! error TS18056: Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.
}

==== ./good.ts (0 errors) ====
import { foo } from "./helpers";
enum A {
a = foo,
b = 3,
}
enum B {
a = 1 + 1,
b,
}
enum C {
a = +2,
b,
}
enum D {
a = (2),
b,
}

@@ -0,0 +1,22 @@
bad.ts(3,8): error TS18055: A member initializer in a enum declaration for a string value must be a string literal when 'isolatedModules' is enabled.


==== ./helpers.ts (0 errors) ====
export const foo = 2;

==== ./bad.ts (1 errors) ====
import { foo } from "./helpers";
enum A {
a = `${foo}`
~~~~~~~~
!!! error TS18055: A member initializer in a enum declaration for a string value must be a string literal when 'isolatedModules' is enabled.
}

==== ./good.ts (0 errors) ====
enum A {
a = `${"foo"}`,
b = "" + 2,
c = 2 + "",
d = ("foo"),
}

@@ -1,3 +1,4 @@
enum2.ts(2,9): error TS18055: A member initializer in a enum declaration for a string value must be a string literal when 'isolatedModules' is enabled.
enum2.ts(3,9): error TS1281: Cannot access 'A' from another file without qualification when 'isolatedModules' is enabled. Use 'Enum.A' instead.
enum2.ts(4,9): error TS1281: Cannot access 'X' from another file without qualification when 'isolatedModules' is enabled. Use 'Enum.X' instead.
script-namespaces.ts(1,11): error TS1280: Namespaces are not allowed in global script files when 'isolatedModules' is enabled. If this file is not intended to be a global script, set 'moduleDetection' to 'force' or add an empty 'export {}' statement.
Expand Down Expand Up @@ -26,9 +27,11 @@ script-namespaces.ts(1,11): error TS1280: Namespaces are not allowed in global s
declare enum Enum { X = 1_000_000 }
const d = 'd';

==== enum2.ts (2 errors) ====
==== enum2.ts (3 errors) ====
enum Enum {
D = d,
~
!!! error TS18055: A member initializer in a enum declaration for a string value must be a string literal when 'isolatedModules' is enabled.
E = A, // error
~
!!! error TS1281: Cannot access 'A' from another file without qualification when 'isolatedModules' is enabled. Use 'Enum.A' instead.
Expand Down
@@ -0,0 +1,32 @@
// @isolatedModules: true
// @noEmit: true
frigus02 marked this conversation as resolved.
Show resolved Hide resolved
// @noTypesAndSymbols: true

// @filename: ./helpers.ts
export const foo = 2;

// @filename: ./bad.ts
import { foo } from "./helpers";
enum A {
a = foo,
b,
}

// @filename: ./good.ts
import { foo } from "./helpers";
enum A {
a = foo,
b = 3,
}
enum B {
a = 1 + 1,
b,
}
enum C {
a = +2,
b,
}
enum D {
a = (2),
b,
}
20 changes: 20 additions & 0 deletions tests/cases/compiler/enumWithNonLiteralStringInitializer.ts
@@ -0,0 +1,20 @@
// @isolatedModules: true
// @noEmit: true
frigus02 marked this conversation as resolved.
Show resolved Hide resolved
// @noTypesAndSymbols: true

// @filename: ./helpers.ts
export const foo = 2;

// @filename: ./bad.ts
import { foo } from "./helpers";
enum A {
a = `${foo}`
}

// @filename: ./good.ts
enum A {
a = `${"foo"}`,
b = "" + 2,
c = 2 + "",
d = ("foo"),
}