fix(transformer): skip enum reverse mappings for syntactic string initializers (#26724)

A member whose value is a string never gets a reverse mapping, but the enum lowering decides that with `is_string_literal()`, so any string-valued initializer the constant evaluator cannot fold is lowered like a number. The inner assignment then produces a string, and the outer write adds a bogus key that can overwrite a real member: with `const foo = "x"`, an enum containing `"xa" = 1` and `A = foo + "a"` ends up with `E["xa"] === "A"` instead of `1`.

Decide syntactically, the way tsc does in `isSyntacticallyString`: string literals, template literals, and additions with a string on either side are strings, looking through parentheses only. An `as`, `satisfies`, non-null, or angle bracket assertion around a string is not syntactically a string, so `("x" as string)` keeps its reverse mapping. Anything else, including `typeof x` and `1 + foo`, keeps its reverse mapping too, matching tsc's emit exactly.

Noticed while auditing enum lowering for #26242; the mis-lowering predates that work and affects plain code, so it targets main directly. The lone-surrogate connection is indirect: on main this path is not reached for `"\uD800" + "\uDC00"`, because the constant evaluator still folds it through the marker encoding. The JSStr migration makes the evaluator decline such strings, and the initializer then falls through to this classifier. With this rule it lowers as a string, so it cannot overwrite a sibling member named by the pair's value. A regression test for that case follows on the migration stack, where it can pass.

## Example

```ts
enum E { A = foo + "a" }
```

Before:

```js
E[E["A"] = foo + "a"] = "A";
```

After, matching tsc:

```js
E["A"] = foo + "a";
```

Verified with a fixture pinning thirteen initializer shapes (`foo + "a"`, `typeof foo`, `` `${foo}` ``, `f()`, parenthesized, mixed `+` chains, `as`/`satisfies` wrappers, and the directly wrapped strings `("x" as string)`, `"x"!`, `<string>"x"`, `("x")`) against tsc 6.0.3 output line for line; the transform conformance snapshot gains only this fixture, and transformer tests pass.

Implemented with AI assistance.
This commit is contained in:
Dunqing committed 2026-09-18 03:07:45 +00:00
1 parent b01bcd9b03
commit 32305e20e5
5 files changed
+64 -7

No files matched your search

+27 -1
View File
@@ -390,7 +390,7 @@ impl<'a> TypeScriptEnum {
Self::get_number_literal_expression(0.0, ctx)
};
let is_str = init.is_string_literal();
let is_str = Self::is_syntactically_string(&init);
// Foo["x"] = init
let member_expr = {
@@ -489,6 +489,32 @@ impl<'a> TypeScriptEnum {
})
}
/// Whether an enum member initializer is a string by syntax alone.
///
/// A string member gets no reverse mapping. tsc decides this without type
/// information: string literals, template literals, and `+` with a string on
/// either side are strings, looking through parentheses and type wrappers.
/// Anything else, including `typeof x`, keeps the reverse mapping.
///
/// This also covers initializers the constant evaluator declined. Emitting a
/// reverse mapping for those wrote a bogus key that could overwrite another
/// member.
///
/// See `isSyntacticallyString` in TypeScript's checker. TypeScript looks
/// through parentheses only, so an `as`, `satisfies`, non-null, or angle
/// bracket assertion around a string still gets a reverse mapping.
fn is_syntactically_string(expr: &Expression<'a>) -> bool {
match expr.without_parentheses() {
Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => true,
Expression::BinaryExpression(binary) => {
binary.operator == BinaryOperator::Addition
&& (Self::is_syntactically_string(&binary.left)
|| Self::is_syntactically_string(&binary.right))
}
_ => false,
}
}
fn get_number_literal_expression(value: f64, ctx: &TraverseCtx<'a>) -> Expression<'a> {
Expression::new_numeric_literal(SPAN, value, None, NumberBase::Decimal, ctx)
}
@@ -1764,8 +1764,8 @@ Missing ReferenceId: "Foo"
Missing ReferenceId: "Foo"
Missing ReferenceId: "Foo"
Symbol reference IDs mismatch for "Foo":
after transform: SymbolId(10): [ReferenceId(9), ReferenceId(10), ReferenceId(11), ReferenceId(12), ReferenceId(13), ReferenceId(14), ReferenceId(15), ReferenceId(16), ReferenceId(17), ReferenceId(18), ReferenceId(19), ReferenceId(20), ReferenceId(21), ReferenceId(22), ReferenceId(23), ReferenceId(24)]
rebuilt : SymbolId(2): [ReferenceId(0), ReferenceId(2), ReferenceId(3), ReferenceId(5), ReferenceId(6), ReferenceId(8), ReferenceId(9), ReferenceId(11), ReferenceId(12), ReferenceId(14), ReferenceId(15), ReferenceId(16), ReferenceId(17), ReferenceId(18), ReferenceId(19), ReferenceId(21), ReferenceId(22), ReferenceId(23), ReferenceId(24)]
after transform: SymbolId(10): [ReferenceId(9), ReferenceId(10), ReferenceId(11), ReferenceId(12), ReferenceId(13), ReferenceId(14), ReferenceId(15), ReferenceId(16), ReferenceId(17), ReferenceId(18), ReferenceId(19), ReferenceId(20), ReferenceId(21), ReferenceId(22)]
rebuilt : SymbolId(2): [ReferenceId(0), ReferenceId(2), ReferenceId(4), ReferenceId(6), ReferenceId(7), ReferenceId(9), ReferenceId(10), ReferenceId(12), ReferenceId(13), ReferenceId(14), ReferenceId(15), ReferenceId(16), ReferenceId(17), ReferenceId(19), ReferenceId(20), ReferenceId(21), ReferenceId(22)]
semantic Error: tasks/coverage/typescript/tests/cases/compiler/computedEnumMemberSyntacticallyString2.ts
Missing ReferenceId: "Foo"
@@ -1774,8 +1774,8 @@ Missing ReferenceId: "Foo"
Missing ReferenceId: "Foo"
Missing ReferenceId: "Foo"
Symbol reference IDs mismatch for "Foo":
after transform: SymbolId(14): [ReferenceId(12), ReferenceId(13), ReferenceId(14), ReferenceId(15), ReferenceId(16), ReferenceId(17), ReferenceId(18), ReferenceId(19), ReferenceId(20), ReferenceId(21), ReferenceId(22), ReferenceId(23), ReferenceId(24), ReferenceId(25), ReferenceId(26), ReferenceId(27), ReferenceId(28), ReferenceId(29), ReferenceId(30), ReferenceId(31), ReferenceId(32), ReferenceId(33)]
rebuilt : SymbolId(3): [ReferenceId(0), ReferenceId(2), ReferenceId(3), ReferenceId(5), ReferenceId(6), ReferenceId(7), ReferenceId(8), ReferenceId(9), ReferenceId(10), ReferenceId(11), ReferenceId(12), ReferenceId(14), ReferenceId(15), ReferenceId(17), ReferenceId(18), ReferenceId(20), ReferenceId(21), ReferenceId(23), ReferenceId(24), ReferenceId(25), ReferenceId(26), ReferenceId(27), ReferenceId(28), ReferenceId(30), ReferenceId(31), ReferenceId(32), ReferenceId(33)]
after transform: SymbolId(14): [ReferenceId(12), ReferenceId(13), ReferenceId(14), ReferenceId(15), ReferenceId(16), ReferenceId(17), ReferenceId(18), ReferenceId(19), ReferenceId(20), ReferenceId(21), ReferenceId(22), ReferenceId(23), ReferenceId(24), ReferenceId(25), ReferenceId(26), ReferenceId(27), ReferenceId(28), ReferenceId(29), ReferenceId(30), ReferenceId(31), ReferenceId(32)]
rebuilt : SymbolId(3): [ReferenceId(0), ReferenceId(2), ReferenceId(3), ReferenceId(5), ReferenceId(6), ReferenceId(7), ReferenceId(8), ReferenceId(9), ReferenceId(10), ReferenceId(11), ReferenceId(13), ReferenceId(14), ReferenceId(16), ReferenceId(17), ReferenceId(19), ReferenceId(20), ReferenceId(22), ReferenceId(23), ReferenceId(24), ReferenceId(25), ReferenceId(26), ReferenceId(27), ReferenceId(29), ReferenceId(30), ReferenceId(31), ReferenceId(32)]
semantic Error: tasks/coverage/typescript/tests/cases/compiler/computedEnumTypeWidening.ts
Bindings mismatch:
@@ -1,6 +1,6 @@
commit: 1eac4481
Passed: 279/407
Passed: 280/408
# All Passed:
* babel-plugin-transform-class-static-block
@@ -48,7 +48,7 @@ x Output mismatch
x Output mismatch
# babel-plugin-transform-typescript (42/61)
# babel-plugin-transform-typescript (43/62)
* allow-declare-fields-false/input.ts
Unresolved references mismatch:
after transform: ["dce"]
@@ -0,0 +1,15 @@
enum F {
A = foo + "a",
B = typeof foo,
C = `${foo}`,
D = f(),
E = (foo + "a"),
G = "a" + foo + 1,
H = 1 + foo,
I = (foo as string) + "a",
J = foo satisfies string,
K = ("x" as string),
L = "x"!,
M = <string>"x",
N = ("x"),
}
@@ -0,0 +1,16 @@
var F = function(F) {
F["A"] = foo + "a";
F[F["B"] = typeof foo] = "B";
F["C"] = `${foo}`;
F[F["D"] = f()] = "D";
F["E"] = foo + "a";
F["G"] = "a" + foo + 1;
F[F["H"] = 1 + foo] = "H";
F["I"] = foo + "a";
F[F["J"] = foo] = "J";
F[F["K"] = "x"] = "K";
F[F["L"] = "x"] = "L";
F[F["M"] = "x"] = "M";
F["N"] = "x";
return F;
}(F || {});