Skip to content

Commit

Permalink
isolatedModules errors for non-literal enum initializers (#56736)
Browse files Browse the repository at this point in the history
  • Loading branch information
frigus02 authored Mar 20, 2024
1 parent af81456 commit f70b068
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 7 deletions.
41 changes: 35 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@ import {
isStringOrNumericLiteralLike,
isSuperCall,
isSuperProperty,
isSyntacticallyString,
isTaggedTemplateExpression,
isTemplateSpan,
isThisContainerOrFunctionBlock,
Expand Down Expand Up @@ -45675,15 +45676,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 @@ -45705,11 +45708,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 (getIsolatedModules(compilerOptions) && previous?.initializer && !isSyntacticallyNumericConstant(previous.initializer)) {
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 @@ -45725,6 +45734,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value,
);
}
else if (getIsolatedModules(compilerOptions) && typeof value === "string" && !isSyntacticallyString(initializer)) {
error(
initializer,
Diagnostics._0_has_a_string_type_but_must_have_syntactically_recognizable_string_syntax_when_isolatedModules_is_enabled,
`${idText(member.parent.name)}.${getTextOfPropertyName(member.name)}`,
);
}
}
else if (isConstEnum) {
error(initializer, Diagnostics.const_enum_member_initializers_must_be_constant_expressions);
Expand All @@ -45738,6 +45754,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return value;
}

function isSyntacticallyNumericConstant(expr: Expression): boolean {
expr = skipOuterExpressions(expr);
switch (expr.kind) {
case SyntaxKind.PrefixUnaryExpression:
return isSyntacticallyNumericConstant((expr as PrefixUnaryExpression).operand);
case SyntaxKind.BinaryExpression:
return isSyntacticallyNumericConstant((expr as BinaryExpression).left) && isSyntacticallyNumericConstant((expr as BinaryExpression).right);
case SyntaxKind.NumericLiteral:
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
Original file line number Diff line number Diff line change
Expand Up @@ -7976,5 +7976,13 @@
"'await using' statements cannot be used inside a class static block.": {
"category": "Error",
"code": 18054
},
"'{0}' has a string type, but must have syntactically recognizable string syntax when 'isolatedModules' is enabled.": {
"category": "Error",
"code": 18055
},
"Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.": {
"category": "Error",
"code": 18056
}
}
Original file line number Diff line number Diff line change
@@ -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,
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//// [tests/cases/compiler/enumNoInitializerFollowsNonLiteralInitializer.ts] ////

//// [helpers.ts]
export const foo = 2;

//// [bad.ts]
import { foo } from "./helpers";
enum A {
a = foo,
b,
}

//// [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,
}


//// [helpers.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.foo = void 0;
exports.foo = 2;
//// [bad.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var helpers_1 = require("./helpers");
var A;
(function (A) {
A[A["a"] = 2] = "a";
A[A["b"] = 3] = "b";
})(A || (A = {}));
//// [good.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var helpers_1 = require("./helpers");
var A;
(function (A) {
A[A["a"] = 2] = "a";
A[A["b"] = 3] = "b";
})(A || (A = {}));
var B;
(function (B) {
B[B["a"] = 2] = "a";
B[B["b"] = 3] = "b";
})(B || (B = {}));
var C;
(function (C) {
C[C["a"] = 2] = "a";
C[C["b"] = 3] = "b";
})(C || (C = {}));
var D;
(function (D) {
D[D["a"] = 2] = "a";
D[D["b"] = 3] = "b";
})(D || (D = {}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
bad.ts(3,8): error TS18055: 'A.a' has a string type, but must have syntactically recognizable string syntax when 'isolatedModules' is enabled.


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

==== ./bad.ts (1 errors) ====
import { bar } from "./helpers";
enum A {
a = bar,
~~~
!!! error TS18055: 'A.a' has a string type, but must have syntactically recognizable string syntax when 'isolatedModules' is enabled.
}

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

47 changes: 47 additions & 0 deletions tests/baselines/reference/enumWithNonLiteralStringInitializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//// [tests/cases/compiler/enumWithNonLiteralStringInitializer.ts] ////

//// [helpers.ts]
export const foo = 2;
export const bar = "bar";

//// [bad.ts]
import { bar } from "./helpers";
enum A {
a = bar,
}

//// [good.ts]
import { foo } from "./helpers";
enum A {
a = `${foo}`,
b = "" + 2,
c = 2 + "",
d = ("foo"),
}


//// [helpers.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = exports.foo = void 0;
exports.foo = 2;
exports.bar = "bar";
//// [bad.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var helpers_1 = require("./helpers");
var A;
(function (A) {
A["a"] = "bar";
})(A || (A = {}));
//// [good.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var helpers_1 = require("./helpers");
var A;
(function (A) {
A["a"] = "2";
A["b"] = "2";
A["c"] = "2";
A["d"] = "foo";
})(A || (A = {}));
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
enum2.ts(2,9): error TS18055: 'Enum.D' has a string type, but must have syntactically recognizable string syntax 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: 'Enum.D' has a string type, but must have syntactically recognizable string syntax 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @isolatedModules: true
// @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,
}
21 changes: 21 additions & 0 deletions tests/cases/compiler/enumWithNonLiteralStringInitializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// @isolatedModules: true
// @noTypesAndSymbols: true

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

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

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

0 comments on commit f70b068

Please sign in to comment.