fix(transformer/typescript): preserve computed-key static block when class has an empty constructor (#21562)

## Summary

When a TypeScript class used a computed-key static property together with a constructor that has no parameter-property assignments, the early return in `transform_class_fields` dropped the `computed_key_assignment_static_block` that initializes the temp var, leaving it declared but never assigned.

```ts
export class SampleClass {
  static [Symbol.toPrimitive] = "test"
  constructor() {}
}
```

previously emitted:

```js
let _Symbol$toPrimitive;
export class SampleClass {
  static {
    this[_Symbol$toPrimitive] = "test";
  }
  constructor() {}
}
```

Now emits (matching TypeScript's own output under `useDefineForClassFields: false`):

```js
let _Symbol$toPrimitive;
export class SampleClass {
  static {
    _Symbol$toPrimitive = Symbol.toPrimitive;
  }
  static {
    this[_Symbol$toPrimitive] = "test";
  }
  constructor() {}
}
```

## Test plan

- Added fixtures `computed-static-property-with-constructor` (exact repro) and `computed-static-property-with-constructor-super` (super() + multiple computed keys) under `babel-plugin-transform-typescript`.
- Verified output against `tsc` 5.7 with `useDefineForClassFields: false`.

Closes rolldown/rolldown#9121
This commit is contained in:
Dunqing authored and Dunqing committed 2026-04-20 15:58:31 +08:00
1 parent 50e9d26714
commit 91e5bde420
8 files changed
+78 -38

No files matched your search

+22 -12
View File
@@ -1,6 +1,6 @@
use oxc_allocator::{TakeIn, Vec as ArenaVec};
use oxc_ast::ast::*;
use oxc_semantic::ScopeFlags;
use oxc_semantic::{ScopeFlags, ScopeId};
use oxc_span::SPAN;
use oxc_str::Ident;
use oxc_syntax::operator::AssignmentOperator;
@@ -100,6 +100,10 @@ impl<'a> TypeScript<'a> {
)
});
// Static blocks created below must be parented to the class body scope,
// not the current traversal scope (which is the scope enclosing the class).
let class_scope_id = class.scope_id();
let mut constructor = None;
let mut property_assignments = Vec::new();
let mut computed_key_assignments = Vec::new();
@@ -123,7 +127,7 @@ impl<'a> TypeScript<'a> {
// `class C { static x = 1; }` -> `class C { static { this.x = 1; } }`
// `class C { static [x] = 1; }` -> `let _x; class C { static { this[_x] = 1; } }`
let body = ctx.ast.vec1(assignment);
*element = Self::create_class_static_block(body, ctx);
*element = Self::create_class_static_block(body, class_scope_id, ctx);
} else {
property_assignments.push(assignment);
}
@@ -170,7 +174,7 @@ impl<'a> TypeScript<'a> {
.ast
.expression_sequence(SPAN, ctx.ast.vec_from_iter(computed_key_assignments));
let statement = ctx.ast.statement_expression(SPAN, sequence_expression);
Self::create_class_static_block(ctx.ast.vec1(statement), ctx)
Self::create_class_static_block(ctx.ast.vec1(statement), class_scope_id, ctx)
});
if let Some(constructor) = constructor {
@@ -179,8 +183,13 @@ impl<'a> TypeScript<'a> {
let params_assignment = Self::convert_constructor_params(params, ctx);
property_assignments.splice(0..0, params_assignment);
// Exit if there are no property and parameter assignments
if property_assignments.is_empty() {
// No property/parameter assignments to inject, but computed-key temp
// inits still need their static block (e.g. `static [expr] = value` +
// empty `constructor()`), otherwise the temp var is left uninitialized.
if let Some(element) = computed_key_assignment_static_block {
class.body.body.insert(0, element);
}
return;
}
@@ -429,21 +438,22 @@ impl<'a> TypeScript<'a> {
)
}
/// Create `static { body }`
/// Create `static { body }` as a child of `class_scope_id`.
///
/// The enclosing traversal scope is the scope around the class, not the class body,
/// so pass `class.scope_id()` explicitly to parent the static block correctly.
#[inline]
fn create_class_static_block(
body: ArenaVec<'a, Statement<'a>>,
class_scope_id: ScopeId,
ctx: &mut TraverseCtx<'a>,
) -> ClassElement<'a> {
let scope_id = ctx.insert_scope_below_statements(
&body,
let scope_id = ctx.insert_scope_below_statement_from_scope_id(
&body[0],
class_scope_id,
ScopeFlags::StrictMode | ScopeFlags::ClassStaticBlock,
);
ctx.ast.class_element_static_block_with_scope_id(
SPAN,
ctx.ast.vec_from_iter(body),
scope_id,
)
ctx.ast.class_element_static_block_with_scope_id(SPAN, body, scope_id)
}
}
@@ -1,6 +1,6 @@
commit: c543b031
Passed: 220/367
Passed: 222/369
# All Passed:
* babel-plugin-transform-class-static-block
@@ -69,7 +69,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 (15/48)
# babel-plugin-transform-typescript (17/50)
* allow-declare-fields-false/input.ts
Unresolved references mismatch:
after transform: ["dce"]
@@ -614,32 +614,8 @@ Scope parent mismatch:
after transform: ScopeId(12): Some(ScopeId(0))
rebuilt : ScopeId(2): Some(ScopeId(1))
Scope parent mismatch:
after transform: ScopeId(11): Some(ScopeId(0))
rebuilt : ScopeId(3): Some(ScopeId(1))
Scope parent mismatch:
after transform: ScopeId(13): Some(ScopeId(0))
rebuilt : ScopeId(5): Some(ScopeId(4))
Scope parent mismatch:
after transform: ScopeId(16): Some(ScopeId(0))
rebuilt : ScopeId(8): Some(ScopeId(7))
Scope parent mismatch:
after transform: ScopeId(14): Some(ScopeId(0))
rebuilt : ScopeId(9): Some(ScopeId(7))
Scope parent mismatch:
after transform: ScopeId(15): Some(ScopeId(0))
rebuilt : ScopeId(10): Some(ScopeId(7))
Scope parent mismatch:
after transform: ScopeId(17): Some(ScopeId(0))
rebuilt : ScopeId(12): Some(ScopeId(11))
Scope parent mismatch:
after transform: ScopeId(18): Some(ScopeId(0))
rebuilt : ScopeId(17): Some(ScopeId(15))
Scope parent mismatch:
after transform: ScopeId(19): Some(ScopeId(0))
rebuilt : ScopeId(19): Some(ScopeId(15))
Scope parent mismatch:
after transform: ScopeId(20): Some(ScopeId(0))
rebuilt : ScopeId(20): Some(ScopeId(15))
Unresolved reference IDs mismatch for "dce":
after transform: [ReferenceId(0), ReferenceId(1), ReferenceId(4), ReferenceId(9), ReferenceId(12), ReferenceId(14), ReferenceId(17)]
rebuilt : [ReferenceId(5)]
@@ -0,0 +1,8 @@
class Sub extends Base {
static [x()] = 1;
static [y()] = 2;
constructor(a: number) {
super(a);
}
}
@@ -0,0 +1,8 @@
{
"assumptions": {
"setPublicClassFields": true
},
"plugins": [
["transform-typescript"]
]
}
@@ -0,0 +1,15 @@
let _x, _y;
class Sub extends Base {
static {
_x = x(), _y = y();
}
static {
this[_x] = 1;
}
static {
this[_y] = 2;
}
constructor(a) {
super(a);
}
}
@@ -0,0 +1,5 @@
export class SampleClass {
static [Symbol.toPrimitive] = "test";
constructor() {}
}
@@ -0,0 +1,8 @@
{
"assumptions": {
"setPublicClassFields": true
},
"plugins": [
["transform-typescript"]
]
}
@@ -0,0 +1,10 @@
let _Symbol$toPrimitive;
export class SampleClass {
static {
_Symbol$toPrimitive = Symbol.toPrimitive;
}
static {
this[_Symbol$toPrimitive] = "test";
}
constructor() {}
}