fix(transformer): legacy decorator on computed property key leaves variable unassigned (#20430)

This PR has two fixes. The second fix exposed a pre-existing bug that
needed to be fixed first.

- 24a72d7bc: `create_read_write_target` was used for a simple `=`
assignment when creating a temp var for decorated computed property
keys. Should be `create_write_target`.

- f0affc6f4: `transform_class_on_exit` was removing all no-initializer
properties unconditionally. But the legacy decorator embeds an
assignment in the computed key (`[FIELD_NAME]` → `[_a =
FIELD_NAME]`), so removing the property also removes the assignment,
leaving `_a` undefined in `_decorate()`. Keep properties whose computed
keys have side effects, matching TypeScript's
  behavior.

  Fixes #20418

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
This commit is contained in:
babandDunqing authored and GitHub committed 2026-03-17 23:31:26 +08:00
1 parent 7c233f42d2
commit 11f96959b0
9 files changed
+48 -17

No files matched your search

@@ -1115,7 +1115,7 @@ impl<'a> LegacyDecorator<'a> {
// Create a unique binding for the computed property key, and insert it outside of the class
let binding = VarDeclarationsStore::create_uid_var_based_on_node(key, ctx);
let operator = AssignmentOperator::Assign;
let left = binding.create_read_write_target(ctx);
let left = binding.create_write_target(ctx);
let right = key.to_expression_mut().take_in(ctx.ast);
let key_expr = ctx.ast.expression_assignment(SPAN, operator, left, right);
*key = PropertyKey::from(key_expr);
@@ -206,11 +206,7 @@ impl<'a> TypeScript<'a> {
}
}
pub(super) fn transform_class_on_exit(
&self,
class: &mut Class<'a>,
_ctx: &mut TraverseCtx<'a>,
) {
pub(super) fn transform_class_on_exit(&self, class: &mut Class<'a>, ctx: &TraverseCtx<'a>) {
if !self.remove_class_fields_without_initializer {
return;
}
@@ -220,6 +216,9 @@ impl<'a> TypeScript<'a> {
&& prop.value.is_none()
&& !prop.key.is_private_identifier()
{
if let Some(key) = prop.key.as_expression() {
return key_needs_temp_var(key, ctx);
}
return false;
}
true
@@ -22822,9 +22822,6 @@ semantic Error: tasks/coverage/typescript/tests/cases/conformance/esDecorators/c
Bindings mismatch:
after transform: ScopeId(0): ["C", "_decorate", "_decorateMetadata", "_field", "dec", "field3"]
rebuilt : ScopeId(0): ["C", "_decorate", "_decorateMetadata", "_field", "field3"]
Reference flags mismatch for "_field":
after transform: ReferenceId(15): ReferenceFlags(Read | Write)
rebuilt : ReferenceId(2): ReferenceFlags(Write)
Reference symbol mismatch for "dec":
after transform: SymbolId(0) "dec"
rebuilt : <None>
@@ -22864,9 +22861,6 @@ semantic Error: tasks/coverage/typescript/tests/cases/conformance/esDecorators/c
Bindings mismatch:
after transform: ScopeId(0): ["C", "_decorate", "_decorateMetadata", "_defineProperty", "_field", "_field2", "dec", "field3"]
rebuilt : ScopeId(0): ["C", "_decorate", "_decorateMetadata", "_defineProperty", "_field", "_field2", "field3"]
Reference flags mismatch for "_field":
after transform: ReferenceId(15): ReferenceFlags(Read | Write)
rebuilt : ReferenceId(4): ReferenceFlags(Write)
Reference symbol mismatch for "dec":
after transform: SymbolId(0) "dec"
rebuilt : <None>
@@ -22896,9 +22890,6 @@ rebuilt : SymbolId(7): Span { start: 199, end: 200 }
Symbol reference IDs mismatch for "D":
after transform: SymbolId(7): [ReferenceId(24)]
rebuilt : SymbolId(7): []
Reference flags mismatch for "_field":
after transform: ReferenceId(16): ReferenceFlags(Read | Write)
rebuilt : ReferenceId(2): ReferenceFlags(Write)
Reference symbol mismatch for "dec":
after transform: SymbolId(0) "dec"
rebuilt : <None>
@@ -1,6 +1,6 @@
commit: 87a048db
Passed: 204/337
Passed: 206/339
# All Passed:
* babel-plugin-transform-class-static-block
@@ -555,7 +555,7 @@ x Output mismatch
x Output mismatch
# legacy-decorators (6/88)
# legacy-decorators (8/90)
* oxc/class-without-name-with-decorated_class/input.ts
Bindings mismatch:
after transform: ScopeId(0): ["dec"]
@@ -0,0 +1,8 @@
const FIELD_NAME = "myField";
function dec(target: any, key: string) {}
class MyModel {
@dec
[FIELD_NAME] = "value";
}
@@ -0,0 +1,7 @@
var _FIELD_NAME;
const FIELD_NAME = "myField";
function dec(target, key) {}
class MyModel {
[_FIELD_NAME = FIELD_NAME] = "value";
}
babelHelpers.decorate([dec], MyModel.prototype, _FIELD_NAME, void 0);
@@ -0,0 +1,8 @@
const FIELD_NAME = "myField";
function dec(target: any, key: string) {}
class MyModel {
@dec
[FIELD_NAME]: string;
}
@@ -0,0 +1,11 @@
{
"plugins": [
"transform-legacy-decorator",
[
"transform-typescript",
{
"removeClassFieldsWithoutInitializer": true
}
]
]
}
@@ -0,0 +1,7 @@
var _FIELD_NAME;
const FIELD_NAME = "myField";
function dec(target, key) {}
class MyModel {
[_FIELD_NAME = FIELD_NAME];
}
babelHelpers.decorate([dec], MyModel.prototype, _FIELD_NAME, void 0);