Skip to content

Commit c51645a

Browse files
committed
isolatedModules errors for non-literal enum initializers
1 parent 7694530 commit c51645a

7 files changed

+178
-7
lines changed

src/compiler/checker.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45010,15 +45010,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4501045010
if (!(nodeLinks.flags & NodeCheckFlags.EnumValuesComputed)) {
4501145011
nodeLinks.flags |= NodeCheckFlags.EnumValuesComputed;
4501245012
let autoValue: number | undefined = 0;
45013+
let previous: EnumMember | undefined;
4501345014
for (const member of node.members) {
45014-
const value = computeMemberValue(member, autoValue);
45015+
const value = computeMemberValue(member, autoValue, previous);
4501545016
getNodeLinks(member).enumMemberValue = value;
4501645017
autoValue = typeof value === "number" ? value + 1 : undefined;
45018+
previous = member;
4501745019
}
4501845020
}
4501945021
}
4502045022

45021-
function computeMemberValue(member: EnumMember, autoValue: number | undefined) {
45023+
function computeMemberValue(member: EnumMember, autoValue: number | undefined, previous: EnumMember | undefined) {
4502245024
if (isComputedNonLiteralName(member.name)) {
4502345025
error(member.name, Diagnostics.Computed_property_names_are_not_allowed_in_enums);
4502445026
}
@@ -45040,11 +45042,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4504045042
// If the member is the first member in the enum declaration, it is assigned the value zero.
4504145043
// Otherwise, it is assigned the value of the immediately preceding member plus one, and an error
4504245044
// occurs if the immediately preceding member is not a constant enum member.
45043-
if (autoValue !== undefined) {
45044-
return autoValue;
45045+
if (autoValue === undefined) {
45046+
error(member.name, Diagnostics.Enum_member_must_have_initializer);
45047+
return undefined;
4504545048
}
45046-
error(member.name, Diagnostics.Enum_member_must_have_initializer);
45047-
return undefined;
45049+
if (compilerOptions.isolatedModules && previous?.initializer && !evaluatesToNumericLiteral(previous.initializer)) {
45050+
error(
45051+
member.name,
45052+
Diagnostics.Enum_member_following_a_non_literal_numeric_member_must_have_an_initializer_when_isolatedModules_is_enabled,
45053+
);
45054+
}
45055+
return autoValue;
4504845056
}
4504945057

4505045058
function computeConstantValue(member: EnumMember): string | number | undefined {
@@ -45060,6 +45068,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4506045068
Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value,
4506145069
);
4506245070
}
45071+
else if (compilerOptions.isolatedModules && typeof value === "string" && !evaluatesToStringLiteral(initializer)) {
45072+
error(
45073+
initializer,
45074+
Diagnostics.A_member_initializer_in_a_enum_declaration_for_a_string_value_must_be_a_string_literal_when_isolatedModules_is_enabled,
45075+
);
45076+
}
4506345077
}
4506445078
else if (isConstEnum) {
4506545079
error(initializer, Diagnostics.const_enum_member_initializers_must_be_constant_expressions);
@@ -45073,6 +45087,44 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4507345087
return value;
4507445088
}
4507545089

45090+
function evaluatesToNumericLiteral(expr: Expression): boolean {
45091+
switch (expr.kind) {
45092+
case SyntaxKind.PrefixUnaryExpression:
45093+
return evaluatesToNumericLiteral((expr as PrefixUnaryExpression).operand);
45094+
case SyntaxKind.BinaryExpression:
45095+
return evaluatesToNumericLiteral((expr as BinaryExpression).left) && evaluatesToNumericLiteral((expr as BinaryExpression).right);
45096+
case SyntaxKind.ParenthesizedExpression:
45097+
return evaluatesToNumericLiteral((expr as ParenthesizedExpression).expression);
45098+
case SyntaxKind.NumericLiteral:
45099+
return true;
45100+
}
45101+
return false;
45102+
}
45103+
45104+
function evaluatesToStringLiteral(expr: Expression): boolean {
45105+
switch (expr.kind) {
45106+
case SyntaxKind.BinaryExpression:
45107+
const left = (expr as BinaryExpression).left;
45108+
const right = (expr as BinaryExpression).right;
45109+
const leftIsNumeric = evaluatesToNumericLiteral(left);
45110+
const rightIsNumeric = evaluatesToNumericLiteral(right);
45111+
return (
45112+
!(leftIsNumeric && rightIsNumeric) &&
45113+
(evaluatesToStringLiteral(left) || leftIsNumeric) &&
45114+
(evaluatesToStringLiteral(right) || rightIsNumeric) &&
45115+
(expr as BinaryExpression).operatorToken.kind === SyntaxKind.PlusToken
45116+
);
45117+
case SyntaxKind.TemplateExpression:
45118+
return (expr as TemplateExpression).templateSpans.every(span => evaluatesToStringLiteral(span.expression));
45119+
case SyntaxKind.ParenthesizedExpression:
45120+
return evaluatesToStringLiteral((expr as ParenthesizedExpression).expression);
45121+
case SyntaxKind.StringLiteral:
45122+
case SyntaxKind.NoSubstitutionTemplateLiteral:
45123+
return true;
45124+
}
45125+
return false;
45126+
}
45127+
4507645128
function evaluate(expr: Expression, location?: Declaration): string | number | undefined {
4507745129
switch (expr.kind) {
4507845130
case SyntaxKind.PrefixUnaryExpression:

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7936,5 +7936,13 @@
79367936
"'await using' statements cannot be used inside a class static block.": {
79377937
"category": "Error",
79387938
"code": 18054
7939+
},
7940+
"A member initializer in a enum declaration for a string value must be a string literal when 'isolatedModules' is enabled.": {
7941+
"category": "Error",
7942+
"code": 18055
7943+
},
7944+
"Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.": {
7945+
"category": "Error",
7946+
"code": 18056
79397947
}
79407948
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
bad.ts(4,5): error TS18056: Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.
2+
3+
4+
==== ./helpers.ts (0 errors) ====
5+
export const foo = 2;
6+
7+
==== ./bad.ts (1 errors) ====
8+
import { foo } from "./helpers";
9+
enum A {
10+
a = foo,
11+
b,
12+
~
13+
!!! error TS18056: Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.
14+
}
15+
16+
==== ./good.ts (0 errors) ====
17+
import { foo } from "./helpers";
18+
enum A {
19+
a = foo,
20+
b = 3,
21+
}
22+
enum B {
23+
a = 1 + 1,
24+
b,
25+
}
26+
enum C {
27+
a = +2,
28+
b,
29+
}
30+
enum D {
31+
a = (2),
32+
b,
33+
}
34+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
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.
2+
3+
4+
==== ./helpers.ts (0 errors) ====
5+
export const foo = 2;
6+
7+
==== ./bad.ts (1 errors) ====
8+
import { foo } from "./helpers";
9+
enum A {
10+
a = `${foo}`
11+
~~~~~~~~
12+
!!! error TS18055: A member initializer in a enum declaration for a string value must be a string literal when 'isolatedModules' is enabled.
13+
}
14+
15+
==== ./good.ts (0 errors) ====
16+
enum A {
17+
a = `${"foo"}`,
18+
b = "" + 2,
19+
c = 2 + "",
20+
d = ("foo"),
21+
}
22+

tests/baselines/reference/isolatedModulesGlobalNamespacesAndEnums.errors.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
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.
12
enum2.ts(3,9): error TS1281: Cannot access 'A' from another file without qualification when 'isolatedModules' is enabled. Use 'Enum.A' instead.
23
enum2.ts(4,9): error TS1281: Cannot access 'X' from another file without qualification when 'isolatedModules' is enabled. Use 'Enum.X' instead.
34
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.
@@ -26,9 +27,11 @@ script-namespaces.ts(1,11): error TS1280: Namespaces are not allowed in global s
2627
declare enum Enum { X = 1_000_000 }
2728
const d = 'd';
2829

29-
==== enum2.ts (2 errors) ====
30+
==== enum2.ts (3 errors) ====
3031
enum Enum {
3132
D = d,
33+
~
34+
!!! error TS18055: A member initializer in a enum declaration for a string value must be a string literal when 'isolatedModules' is enabled.
3235
E = A, // error
3336
~
3437
!!! error TS1281: Cannot access 'A' from another file without qualification when 'isolatedModules' is enabled. Use 'Enum.A' instead.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// @isolatedModules: true
2+
// @noEmit: true
3+
// @noTypesAndSymbols: true
4+
5+
// @filename: ./helpers.ts
6+
export const foo = 2;
7+
8+
// @filename: ./bad.ts
9+
import { foo } from "./helpers";
10+
enum A {
11+
a = foo,
12+
b,
13+
}
14+
15+
// @filename: ./good.ts
16+
import { foo } from "./helpers";
17+
enum A {
18+
a = foo,
19+
b = 3,
20+
}
21+
enum B {
22+
a = 1 + 1,
23+
b,
24+
}
25+
enum C {
26+
a = +2,
27+
b,
28+
}
29+
enum D {
30+
a = (2),
31+
b,
32+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @isolatedModules: true
2+
// @noEmit: true
3+
// @noTypesAndSymbols: true
4+
5+
// @filename: ./helpers.ts
6+
export const foo = 2;
7+
8+
// @filename: ./bad.ts
9+
import { foo } from "./helpers";
10+
enum A {
11+
a = `${foo}`
12+
}
13+
14+
// @filename: ./good.ts
15+
enum A {
16+
a = `${"foo"}`,
17+
b = "" + 2,
18+
c = 2 + "",
19+
d = ("foo"),
20+
}

0 commit comments

Comments
 (0)