diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fce6f35fb2248..89fc8bcc525a1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -719,6 +719,7 @@ import { isStringOrNumericLiteralLike, isSuperCall, isSuperProperty, + isSyntacticallyString, isTaggedTemplateExpression, isTemplateSpan, isThisContainerOrFunctionBlock, @@ -45518,15 +45519,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); } @@ -45548,11 +45551,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 { @@ -45568,6 +45577,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); @@ -45581,6 +45597,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: diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 4c48402afafbb..dfbb41b3d409d 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -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 } } diff --git a/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.errors.txt b/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.errors.txt new file mode 100644 index 0000000000000..2999b769c9699 --- /dev/null +++ b/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.errors.txt @@ -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, + } + \ No newline at end of file diff --git a/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.js b/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.js new file mode 100644 index 0000000000000..1b24257e04f7c --- /dev/null +++ b/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.js @@ -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 = {})); diff --git a/tests/baselines/reference/enumWithNonLiteralStringInitializer.errors.txt b/tests/baselines/reference/enumWithNonLiteralStringInitializer.errors.txt new file mode 100644 index 0000000000000..0c6a54a6bc8d4 --- /dev/null +++ b/tests/baselines/reference/enumWithNonLiteralStringInitializer.errors.txt @@ -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"), + } + \ No newline at end of file diff --git a/tests/baselines/reference/enumWithNonLiteralStringInitializer.js b/tests/baselines/reference/enumWithNonLiteralStringInitializer.js new file mode 100644 index 0000000000000..a9812bf95e169 --- /dev/null +++ b/tests/baselines/reference/enumWithNonLiteralStringInitializer.js @@ -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 = {})); diff --git a/tests/baselines/reference/isolatedModulesGlobalNamespacesAndEnums.errors.txt b/tests/baselines/reference/isolatedModulesGlobalNamespacesAndEnums.errors.txt index c0c853cca68e6..d5e4778259d40 100644 --- a/tests/baselines/reference/isolatedModulesGlobalNamespacesAndEnums.errors.txt +++ b/tests/baselines/reference/isolatedModulesGlobalNamespacesAndEnums.errors.txt @@ -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. @@ -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. diff --git a/tests/cases/compiler/enumNoInitializerFollowsNonLiteralInitializer.ts b/tests/cases/compiler/enumNoInitializerFollowsNonLiteralInitializer.ts new file mode 100644 index 0000000000000..b4274dbd53385 --- /dev/null +++ b/tests/cases/compiler/enumNoInitializerFollowsNonLiteralInitializer.ts @@ -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, +} diff --git a/tests/cases/compiler/enumWithNonLiteralStringInitializer.ts b/tests/cases/compiler/enumWithNonLiteralStringInitializer.ts new file mode 100644 index 0000000000000..d942313c4eaf1 --- /dev/null +++ b/tests/cases/compiler/enumWithNonLiteralStringInitializer.ts @@ -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"), +}