fix(transformer): inline const enum members through type-cast wrappers (#22500)

Closes #22444.

`Phase.idle as Phase`, `Phase.idle satisfies Phase`, `Phase.idle!`, and `<Phase>Phase.idle` failed to inline under `optimizeConstEnums`. The const enum declaration was stripped while the member access was left as `Phase.idle`, producing a runtime `ReferenceError`.

## Cause

The enum inliner's `enter_expression` matched only bare `StaticMemberExpression`/`ComputedMemberExpression`. `annotations.rs` strips TS-only wrappers in its own `enter_expression`, but `walk_expression` only calls `enter_expression` once per node — after the strip the inner `Phase.idle` was never revisited.

## Fix

Peek through the wrappers via `Expression::get_inner_expression_mut()` (it already handles `TSAsExpression`, `TSSatisfiesExpression`, `TSNonNullExpression`, `TSTypeAssertion`, `TSInstantiationExpression`, `ParenthesizedExpression`). The outer `*expr = …` replacement then drops the wrapper along with the inlined value.

## Example

```ts
const enum Phase { idle, active }

class Foo {
  #foo = Phase.idle as Phase;
}
```

Before:
```js
class Foo {
  #foo = Phase.idle;  // dangling reference
}
```

After:
```js
class Foo {
  #foo = 0;
}
```

## Verification

New fixtures cover all wrapper shapes for const + regular enums:
- `babel-plugin-transform-typescript/const-enum-type-cast/`
- `babel-plugin-transform-typescript/optimize-enums/type-cast/`
This commit is contained in:
Dunqing committed 2026-05-18 02:19:10 +00:00
1 parent c9f506e875
commit 931b7d6b97
7 files changed
+88 -3

No files matched your search

@@ -102,7 +102,11 @@ impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptEnum {
return;
}
let inlined = match expr {
// Peek through TS-only wrappers and parens so `E.X as T`, `E.X satisfies T`,
// `E.X!`, `<T>E.X`, `E.X` (with `preserveParens`) all inline. `annotations.rs`
// strips these wrappers, but only after this hook returns — by then the outer
// node has been replaced and `enter_expression` is not re-invoked on it.
let inlined = match expr.get_inner_expression_mut() {
Expression::StaticMemberExpression(member_expr) => {
self.try_inline_enum_member(member_expr, ctx)
}
@@ -1,6 +1,6 @@
commit: 4079bcda
Passed: 235/382
Passed: 236/384
# All Passed:
* babel-plugin-transform-class-static-block
@@ -63,7 +63,7 @@ after transform: SymbolId(0): [ReferenceId(0), ReferenceId(2), ReferenceId(6), R
rebuilt : SymbolId(0): [ReferenceId(0), ReferenceId(2), ReferenceId(6), ReferenceId(10)]
# babel-plugin-transform-typescript (22/56)
# babel-plugin-transform-typescript (23/58)
* allow-declare-fields-false/input.ts
Unresolved references mismatch:
after transform: ["dce"]
@@ -526,6 +526,20 @@ Symbol flags mismatch for "B":
after transform: SymbolId(2): SymbolFlags(RegularEnum)
rebuilt : SymbolId(2): SymbolFlags(FunctionScopedVariable)
* optimize-enums/type-cast/input.ts
Bindings mismatch:
after transform: ScopeId(1): ["Direction", "Down", "Up"]
rebuilt : ScopeId(1): ["Direction"]
Scope flags mismatch:
after transform: ScopeId(1): ScopeFlags(0x0)
rebuilt : ScopeId(1): ScopeFlags(Function)
Symbol flags mismatch for "Direction":
after transform: SymbolId(0): SymbolFlags(RegularEnum)
rebuilt : SymbolId(0): SymbolFlags(FunctionScopedVariable)
Symbol reference IDs mismatch for "Direction":
after transform: SymbolId(0): [ReferenceId(5), ReferenceId(1), ReferenceId(7), ReferenceId(3), ReferenceId(14)]
rebuilt : SymbolId(0): [ReferenceId(5)]
* optimize-enums/typeof-kept/input.ts
Bindings mismatch:
after transform: ScopeId(1): ["Bar", "X"]
@@ -0,0 +1,29 @@
const enum Phase {
idle,
active,
}
const enum Theme {
Light = "Light",
Dark = "Dark",
}
// `as` type-cast
const a1 = Phase.idle as Phase;
const a2 = Phase["active"] as Phase;
const a3 = Theme.Light as Theme;
// `satisfies` type-cast
const s1 = Phase.idle satisfies Phase;
const s2 = Theme.Dark satisfies Theme;
// `!` non-null assertion
const n1 = Phase.active!;
const n2 = Theme.Light!;
// `<T>` type assertion
const t1 = <Phase>Phase.idle;
// nested / combined
const c1 = (Phase.active as Phase) satisfies Phase;
const c2 = (Phase.idle satisfies Phase)!;
@@ -0,0 +1,3 @@
{
"plugins": [["transform-typescript", { "optimizeConstEnums": true }]]
}
@@ -0,0 +1,15 @@
// `as` type-cast
const a1 = 0;
const a2 = 1;
const a3 = "Light";
// `satisfies` type-cast
const s1 = 0;
const s2 = "Dark";
// `!` non-null assertion
const n1 = 1;
const n2 = "Light";
// `<T>` type assertion
const t1 = 0;
// nested / combined
const c1 = 1;
const c2 = 0;
@@ -0,0 +1,10 @@
enum Direction {
Up,
Down,
}
Direction.Up as Direction;
Direction["Down"] as Direction;
Direction.Up satisfies Direction;
Direction.Down!;
<Direction>Direction.Up;
@@ -0,0 +1,10 @@
var Direction = /* @__PURE__ */ function(Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
return Direction;
}(Direction || {});
0;
1;
0;
1;
0;