refactor(transformer/class-properties): split up code into multiple files (#7912)

Split out methods from `class.rs` into separate files.

Note: `static_block.rs` is currently very small, but it will get bigger - a new visitor is required here to rename references to class name and `this`.
This commit is contained in:
overlookmotel 2024-12-15 14:17:29 +00:00
parent 9989b58b1c
commit 544ffbf6c7
5 changed files with 512 additions and 455 deletions

View file

@ -15,13 +15,9 @@ use oxc_traverse::{BoundIdentifier, TraverseCtx};
use crate::common::helper_loader::Helper;
use super::{
super::ClassStaticBlock,
constructor::InstanceInitsInsertLocation,
private_props::{PrivateProp, PrivateProps},
utils::{
create_assignment, create_underscore_ident_name, create_variable_declaration,
exprs_into_stmts,
},
utils::{create_assignment, create_variable_declaration, exprs_into_stmts},
ClassBindings, ClassProperties, FxIndexMap,
};
@ -530,463 +526,18 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
self.private_props_stack.pop();
}
/// Convert instance property to initialization expression.
/// Property `foo = 123;` -> Expression `this.foo = 123` or `_defineProperty(this, "foo", 123)`.
fn convert_instance_property(
&mut self,
prop: &mut PropertyDefinition<'a>,
instance_inits: &mut Vec<Expression<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
// Get value
let value = match &mut prop.value {
Some(value) => {
self.transform_instance_initializer(value, ctx);
ctx.ast.move_expression(value)
}
None => ctx.ast.void_0(SPAN),
};
let init_expr = if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key {
self.create_private_instance_init_assignment(ident, value, ctx)
} else {
// Convert to assignment or `_defineProperty` call, depending on `loose` option
let this = ctx.ast.expression_this(SPAN);
self.create_init_assignment(prop, value, this, false, ctx)
};
instance_inits.push(init_expr);
}
/// Convert static property to initialization expression.
/// Property `static foo = 123;` -> Expression `C.foo = 123` or `_defineProperty(C, "foo", 123)`.
fn convert_static_property(
&mut self,
prop: &mut PropertyDefinition<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// Get value, and transform it to replace `this` with reference to class name,
// and transform class property accesses (`object.#prop`)
let value = match &mut prop.value {
Some(value) => {
self.transform_static_initializer(value, ctx);
ctx.ast.move_expression(value)
}
None => ctx.ast.void_0(SPAN),
};
if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key {
self.insert_private_static_init_assignment(ident, value, ctx);
} else {
// Convert to assignment or `_defineProperty` call, depending on `loose` option
let class_binding = if self.is_declaration {
// Class declarations always have a name except `export default class {}`.
// For default export, binding is created when static prop found in 1st pass.
self.class_bindings.name.as_ref().unwrap()
} else {
// Binding is created when static prop found in 1st pass.
self.class_bindings.temp.as_ref().unwrap()
};
let assignee = class_binding.create_read_expression(ctx);
let init_expr = self.create_init_assignment(prop, value, assignee, true, ctx);
self.insert_expr_after_class(init_expr, ctx);
}
}
/// `assignee.foo = value` or `_defineProperty(assignee, "foo", value)`
fn create_init_assignment(
&mut self,
prop: &mut PropertyDefinition<'a>,
value: Expression<'a>,
assignee: Expression<'a>,
is_static: bool,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
if self.set_public_class_fields {
// `assignee.foo = value`
self.create_init_assignment_loose(prop, value, assignee, is_static, ctx)
} else {
// `_defineProperty(assignee, "foo", value)`
self.create_init_assignment_not_loose(prop, value, assignee, ctx)
}
}
/// `this.foo = value` or `_Class.foo = value`
fn create_init_assignment_loose(
&mut self,
prop: &mut PropertyDefinition<'a>,
value: Expression<'a>,
assignee: Expression<'a>,
is_static: bool,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
// In-built static props `name` and `length` need to be set with `_defineProperty`
let needs_define = |name| is_static && (name == "name" || name == "length");
let left = match &mut prop.key {
PropertyKey::StaticIdentifier(ident) => {
if needs_define(&ident.name) {
return self.create_init_assignment_not_loose(prop, value, assignee, ctx);
}
ctx.ast.member_expression_static(SPAN, assignee, ident.as_ref().clone(), false)
}
PropertyKey::StringLiteral(str_lit) if needs_define(&str_lit.value) => {
return self.create_init_assignment_not_loose(prop, value, assignee, ctx);
}
key @ match_expression!(PropertyKey) => {
// TODO: This can also be a numeric key (non-computed). Maybe other key types?
let key = self.create_computed_key_temp_var(key.to_expression_mut(), ctx);
ctx.ast.member_expression_computed(SPAN, assignee, key, false)
}
PropertyKey::PrivateIdentifier(_) => {
// Handled in `convert_instance_property` and `convert_static_property`
unreachable!();
}
};
// TODO: Should this have span of the original `PropertyDefinition`?
ctx.ast.expression_assignment(
SPAN,
AssignmentOperator::Assign,
AssignmentTarget::from(left),
value,
)
}
/// `_defineProperty(this, "foo", value)` or `_defineProperty(_Class, "foo", value)`
fn create_init_assignment_not_loose(
&mut self,
prop: &mut PropertyDefinition<'a>,
value: Expression<'a>,
assignee: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let key = match &mut prop.key {
PropertyKey::StaticIdentifier(ident) => {
ctx.ast.expression_string_literal(ident.span, ident.name.clone(), None)
}
key @ match_expression!(PropertyKey) => {
// TODO: This can also be a numeric key (non-computed). Maybe other key types?
self.create_computed_key_temp_var(key.to_expression_mut(), ctx)
}
PropertyKey::PrivateIdentifier(_) => {
// Handled in `convert_instance_property` and `convert_static_property`
unreachable!();
}
};
let arguments = ctx.ast.vec_from_array([
Argument::from(assignee),
Argument::from(key),
Argument::from(value),
]);
// TODO: Should this have span of the original `PropertyDefinition`?
self.ctx.helper_call_expr(Helper::DefineProperty, SPAN, arguments, ctx)
}
/// Create init assignment for private instance prop, to be inserted into class constructor.
///
/// Loose: `Object.defineProperty(this, _prop, {writable: true, value: value})`
/// Not loose: `_classPrivateFieldInitSpec(this, _prop, value)`
fn create_private_instance_init_assignment(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
if self.private_fields_as_properties {
let this = ctx.ast.expression_this(SPAN);
self.create_private_init_assignment_loose(ident, value, this, ctx)
} else {
self.create_private_instance_init_assignment_not_loose(ident, value, ctx)
}
}
/// `Object.defineProperty(<assignee>, _prop, {writable: true, value: value})`
fn create_private_init_assignment_loose(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
assignee: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
// `Object.defineProperty`
let object_symbol_id = ctx.scopes().find_binding(ctx.current_scope_id(), "Object");
let object = ctx.create_ident_expr(
SPAN,
Atom::from("Object"),
object_symbol_id,
ReferenceFlags::Read,
);
let property = ctx.ast.identifier_name(SPAN, "defineProperty");
let callee =
Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false));
// `{writable: true, value: <value>}`
let prop_def = ctx.ast.expression_object(
SPAN,
ctx.ast.vec_from_array([
ctx.ast.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
ctx.ast.property_key_identifier_name(SPAN, Atom::from("writable")),
ctx.ast.expression_boolean_literal(SPAN, true),
false,
false,
false,
),
ctx.ast.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
ctx.ast.property_key_identifier_name(SPAN, Atom::from("value")),
value,
false,
false,
false,
),
]),
None,
);
let private_props = self.private_props_stack.last().unwrap();
let prop = &private_props.props[&ident.name];
let arguments = ctx.ast.vec_from_array([
Argument::from(assignee),
Argument::from(prop.binding.create_read_expression(ctx)),
Argument::from(prop_def),
]);
// TODO: Should this have span of original `PropertyDefinition`?
ctx.ast.expression_call(SPAN, callee, NONE, arguments, false)
}
/// `_classPrivateFieldInitSpec(this, _prop, value)`
fn create_private_instance_init_assignment_not_loose(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let private_props = self.private_props_stack.last().unwrap();
let prop = &private_props.props[&ident.name];
let arguments = ctx.ast.vec_from_array([
Argument::from(ctx.ast.expression_this(SPAN)),
Argument::from(prop.binding.create_read_expression(ctx)),
Argument::from(value),
]);
// TODO: Should this have span of original `PropertyDefinition`?
self.ctx.helper_call_expr(Helper::ClassPrivateFieldInitSpec, SPAN, arguments, ctx)
}
/// Insert after class:
///
/// Not loose:
/// * Class declaration: `var _prop = {_: value};`
/// * Class expression: `_prop = {_: value}`
///
/// Loose:
/// `Object.defineProperty(Class, _prop, {writable: true, value: value});`
fn insert_private_static_init_assignment(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if self.private_fields_as_properties {
self.insert_private_static_init_assignment_loose(ident, value, ctx);
} else {
self.insert_private_static_init_assignment_not_loose(ident, value, ctx);
}
}
/// Insert after class:
/// `Object.defineProperty(Class, _prop, {writable: true, value: value});`
fn insert_private_static_init_assignment_loose(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// TODO: This logic appears elsewhere. De-duplicate it.
let class_binding = if self.is_declaration {
// Class declarations always have a name except `export default class {}`.
// For default export, binding is created when static prop found in 1st pass.
self.class_bindings.name.as_ref().unwrap()
} else {
// Binding is created when static prop found in 1st pass.
self.class_bindings.temp.as_ref().unwrap()
};
let assignee = class_binding.create_read_expression(ctx);
let assignment = self.create_private_init_assignment_loose(ident, value, assignee, ctx);
self.insert_expr_after_class(assignment, ctx);
}
/// Insert after class:
///
/// * Class declaration: `var _prop = {_: value};`
/// * Class expression: `_prop = {_: value}`
fn insert_private_static_init_assignment_not_loose(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// `_prop = {_: value}`
let property = ctx.ast.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
PropertyKey::StaticIdentifier(ctx.ast.alloc(create_underscore_ident_name(ctx))),
value,
false,
false,
false,
);
let obj = ctx.ast.expression_object(SPAN, ctx.ast.vec1(property), None);
// Insert after class
let private_props = self.private_props_stack.last().unwrap();
let prop = &private_props.props[&ident.name];
if self.is_declaration {
// `var _prop = {_: value};`
let var_decl = create_variable_declaration(&prop.binding, obj, ctx);
self.insert_after_stmts.push(var_decl);
} else {
// `_prop = {_: value}`
let assignment = create_assignment(&prop.binding, obj, ctx);
self.insert_after_exprs.push(assignment);
}
}
/// Substitute temp var for method computed key.
/// `class C { [x()]() {} }` -> `let _x; _x = x(); class C { [_x]() {} }`
/// This transform is only required if class has properties or a static block.
fn substitute_temp_var_for_method_computed_key(
&mut self,
method: &mut MethodDefinition<'a>,
ctx: &mut TraverseCtx<'a>,
) {
let Some(key) = method.key.as_expression_mut() else { return };
// TODO: Don't alter key if it's provable evaluating it has no side effects.
// TODO(improve-on-babel): It's unnecessary to create temp vars for method keys unless:
// 1. Properties also have computed keys.
// 2. Some of those properties' computed keys have side effects and require temp vars.
// 3. At least one property satisfying the above is after this method,
// or class contains a static block which is being transformed
// (static blocks are always evaluated after computed keys, regardless of order)
method.key = PropertyKey::from(self.create_computed_key_temp_var(key, ctx));
}
/// Convert static block to `Expression`.
///
/// `static { x = 1; }` -> `x = 1`
/// `static { x = 1; y = 2; } -> `(() => { x = 1; y = 2; })()`
///
/// TODO: Add tests for this if there aren't any already.
/// Include tests for evaluation order inc that static block goes before class expression
/// unless also static properties, or static block uses class name.
fn convert_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) {
// TODO: Convert `this` and references to class name.
// `x = class C { static { this.C = C; } }` -> `x = (_C = class C {}, _C.C = _C, _C)`
// TODO: Scope of static block contents becomes outer scope, not scope of class.
// If class expression, assignment in static block moves to a position where it's read from.
// e.g.: `x` here now has read+write `ReferenceFlags`:
// `C = class C { static { x = 1; } }` -> `C = (_C = class C {}, x = 1, _C)`
let expr = ClassStaticBlock::convert_block_to_expression(block, ctx);
self.insert_expr_after_class(expr, ctx);
}
/// Insert an expression after the class.
fn insert_expr_after_class(&mut self, expr: Expression<'a>, ctx: &mut TraverseCtx<'a>) {
pub(super) fn insert_expr_after_class(
&mut self,
expr: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if self.is_declaration {
self.insert_after_stmts.push(ctx.ast.statement_expression(SPAN, expr));
} else {
self.insert_after_exprs.push(expr);
}
}
/// Convert computed property/method key to a temp var.
///
/// Transformation is:
/// * Class declaration:
/// `class C { [x()] = 1; }` -> `let _x; _x = x(); class C { constructor() { this[_x] = 1; } }`
/// * Class expression:
/// `C = class { [x()] = 1; }` -> `let _x; C = (_x = x(), class C { constructor() { this[_x] = 1; } })`
///
/// This function:
/// * Creates the `let _x;` statement and inserts it.
/// * Creates the `_x = x()` assignments.
/// * Inserts assignments before class declaration, or adds to `state` if class expression.
/// * Returns `_x`.
fn create_computed_key_temp_var(
&mut self,
key: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let key = ctx.ast.move_expression(key);
// Bound vars and literals do not need temp var - return unchanged.
// e.g. `let x = 'x'; class C { [x] = 1; }` or `class C { ['x'] = 1; }`
//
// `this` does not have side effects, but it needs a temp var anyway, because `this` in computed
// key and `this` within class constructor resolve to different `this` bindings.
// So we need to create a temp var outside of the class to get the correct `this`.
// `class C { [this] = 1; }`
// -> `let _this; _this = this; class C { constructor() { this[_this] = 1; } }`
//
// TODO(improve-on-babel): Can avoid the temp var if key is for a static prop/method,
// as in that case the usage of `this` stays outside the class.
//
// TODO: Do fuller analysis to detect expressions which cannot have side effects e.g. `'x' + 'y'`.
let cannot_have_side_effects = match &key {
Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::StringLiteral(_) => true,
Expression::Identifier(ident) => {
// Cannot have side effects if is bound.
// Additional check that the var is not mutated is required for cases like
// `let x = 1; class { [x] = 1; [++x] = 2; }`
// `++x` is hoisted to before class in output, so `x` in 1st key would get the wrong
// value unless it's hoisted out too.
// TODO: Add an exec test for this odd case.
// TODO(improve-on-babel): That case is rare.
// Test for it in first pass over class elements, and avoid temp vars where possible.
match ctx.symbols().get_reference(ident.reference_id()).symbol_id() {
Some(symbol_id) => {
ctx.symbols().get_flags(symbol_id).contains(SymbolFlags::ConstVariable)
|| ctx
.symbols()
.get_resolved_references(symbol_id)
.all(|reference| !reference.is_write())
}
None => false,
}
}
_ => false,
};
if cannot_have_side_effects {
return key;
}
// We entered transform via `enter_expression` or `enter_statement`,
// so `ctx.current_scope_id()` is the scope outside the class
let parent_scope_id = ctx.current_scope_id();
// TODO: Handle if is a class expression defined in a function's params.
let binding =
ctx.generate_uid_based_on_node(&key, parent_scope_id, SymbolFlags::BlockScopedVariable);
self.ctx.var_declarations.insert_let(&binding, None, ctx);
let assignment = create_assignment(&binding, key, ctx);
self.insert_before.push(assignment);
binding.create_read_expression(ctx)
}
}
/// Create `new WeakMap()` expression.

View file

@ -0,0 +1,114 @@
//! ES2022: Class Properties
//! Transform of class property/method computed keys.
use oxc_ast::ast::*;
use oxc_syntax::symbol::SymbolFlags;
use oxc_traverse::TraverseCtx;
use super::{utils::create_assignment, ClassProperties};
impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
/// Substitute temp var for method computed key.
/// `class C { [x()]() {} }` -> `let _x; _x = x(); class C { [_x]() {} }`
/// This transform is only required if class has properties or a static block.
pub(super) fn substitute_temp_var_for_method_computed_key(
&mut self,
method: &mut MethodDefinition<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// TODO: Don't alter numeric literal key e.g. `class C { 123() {} }`
// TODO: Don't re-create key if it doesn't need to be altered
let Some(key) = method.key.as_expression_mut() else { return };
// TODO: Don't alter key if it's provable evaluating it has no side effects.
// TODO(improve-on-babel): It's unnecessary to create temp vars for method keys unless:
// 1. Properties also have computed keys.
// 2. Some of those properties' computed keys have side effects and require temp vars.
// 3. At least one property satisfying the above is after this method,
// or class contains a static block which is being transformed
// (static blocks are always evaluated after computed keys, regardless of order)
method.key = PropertyKey::from(self.create_computed_key_temp_var(key, ctx));
}
/// Convert computed property/method key to a temp var.
///
/// Transformation is:
/// * Class declaration:
/// `class C { [x()] = 1; }` -> `let _x; _x = x(); class C { constructor() { this[_x] = 1; } }`
/// * Class expression:
/// `C = class { [x()] = 1; }` -> `let _x; C = (_x = x(), class C { constructor() { this[_x] = 1; } })`
///
/// This function:
/// * Creates the `let _x;` statement and inserts it.
/// * Creates the `_x = x()` assignments.
/// * Inserts assignments before class declaration, or adds to `state` if class expression.
/// * Returns `_x`.
pub(super) fn create_computed_key_temp_var(
&mut self,
key: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let key = ctx.ast.move_expression(key);
// Bound vars and literals do not need temp var - return unchanged.
// e.g. `let x = 'x'; class C { [x] = 1; }` or `class C { ['x'] = 1; }`
//
// `this` does not have side effects, but it needs a temp var anyway, because `this` in computed
// key and `this` within class constructor resolve to different `this` bindings.
// So we need to create a temp var outside of the class to get the correct `this`.
// `class C { [this] = 1; }`
// -> `let _this; _this = this; class C { constructor() { this[_this] = 1; } }`
//
// TODO(improve-on-babel): Can avoid the temp var if key is for a static prop/method,
// as in that case the usage of `this` stays outside the class.
//
// TODO: Do fuller analysis to detect expressions which cannot have side effects e.g. `'x' + 'y'`.
let cannot_have_side_effects = match &key {
Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::StringLiteral(_) => true,
Expression::Identifier(ident) => {
// Cannot have side effects if is bound.
// Additional check that the var is not mutated is required for cases like
// `let x = 1; class { [x] = 1; [++x] = 2; }`
// `++x` is hoisted to before class in output, so `x` in 1st key would get the wrong
// value unless it's hoisted out too.
// TODO: Add an exec test for this odd case.
// TODO(improve-on-babel): That case is rare.
// Test for it in first pass over class elements, and avoid temp vars where possible.
match ctx.symbols().get_reference(ident.reference_id()).symbol_id() {
Some(symbol_id) => {
// TODO: Use `SymbolTable::symbol_is_mutated`
ctx.symbols().get_flags(symbol_id).contains(SymbolFlags::ConstVariable)
|| ctx
.symbols()
.get_resolved_references(symbol_id)
.all(|reference| !reference.is_write())
}
None => false,
}
}
_ => false,
};
if cannot_have_side_effects {
return key;
}
// We entered transform via `enter_expression` or `enter_statement`,
// so `ctx.current_scope_id()` is the scope outside the class
let parent_scope_id = ctx.current_scope_id();
// TODO: Handle if is a class expression defined in a function's params.
let binding =
ctx.generate_uid_based_on_node(&key, parent_scope_id, SymbolFlags::BlockScopedVariable);
self.ctx.var_declarations.insert_let(&binding, None, ctx);
let assignment = create_assignment(&binding, key, ctx);
self.insert_before.push(assignment);
binding.create_read_expression(ctx)
}
}

View file

@ -127,9 +127,12 @@
//!
//! * `mod.rs`: Setup and visitor.
//! * `class.rs`: Transform of class body.
//! * `prop_decl.rs`: Transform of property declarations (instance and static).
//! * `constructor.rs`: Insertion of property initializers into class constructor.
//! * `instance_prop_init.rs`: Transform of instance property initializers.
//! * `static_prop_init.rs`: Transform of static property initializers.
//! * `static_block.rs`: Transform of static blocks.
//! * `computed_key.rs`: Transform of property/method computed keys.
//! * `private_field.rs`: Transform of private fields (`this.#prop`).
//! * `super.rs`: Transform `super` expressions.
//! * `class_bindings.rs`: Structure containing bindings for class name and temp var.
@ -159,10 +162,13 @@ use crate::TransformCtx;
mod class;
mod class_bindings;
mod computed_key;
mod constructor;
mod instance_prop_init;
mod private_field;
mod private_props;
mod prop_decl;
mod static_block;
mod static_prop_init;
mod supers;
mod utils;

View file

@ -0,0 +1,351 @@
//! ES2022: Class Properties
//! Transform of class property declarations (instance or static properties).
use oxc_ast::{ast::*, NONE};
use oxc_span::SPAN;
use oxc_syntax::reference::ReferenceFlags;
use oxc_traverse::TraverseCtx;
use crate::common::helper_loader::Helper;
use super::{
utils::{create_assignment, create_underscore_ident_name, create_variable_declaration},
ClassProperties,
};
// Instance properties
impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
/// Convert instance property to initialization expression.
/// Property `foo = 123;` -> Expression `this.foo = 123` or `_defineProperty(this, "foo", 123)`.
pub(super) fn convert_instance_property(
&mut self,
prop: &mut PropertyDefinition<'a>,
instance_inits: &mut Vec<Expression<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
// Get value
let value = match &mut prop.value {
Some(value) => {
self.transform_instance_initializer(value, ctx);
ctx.ast.move_expression(value)
}
None => ctx.ast.void_0(SPAN),
};
let init_expr = if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key {
self.create_private_instance_init_assignment(ident, value, ctx)
} else {
// Convert to assignment or `_defineProperty` call, depending on `loose` option
let this = ctx.ast.expression_this(SPAN);
self.create_init_assignment(prop, value, this, false, ctx)
};
instance_inits.push(init_expr);
}
/// Create init assignment for private instance prop, to be inserted into class constructor.
///
/// Loose: `Object.defineProperty(this, _prop, {writable: true, value: value})`
/// Not loose: `_classPrivateFieldInitSpec(this, _prop, value)`
fn create_private_instance_init_assignment(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
if self.private_fields_as_properties {
let this = ctx.ast.expression_this(SPAN);
self.create_private_init_assignment_loose(ident, value, this, ctx)
} else {
self.create_private_instance_init_assignment_not_loose(ident, value, ctx)
}
}
/// `_classPrivateFieldInitSpec(this, _prop, value)`
fn create_private_instance_init_assignment_not_loose(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let private_props = self.private_props_stack.last().unwrap();
let prop = &private_props.props[&ident.name];
let arguments = ctx.ast.vec_from_array([
Argument::from(ctx.ast.expression_this(SPAN)),
Argument::from(prop.binding.create_read_expression(ctx)),
Argument::from(value),
]);
// TODO: Should this have span of original `PropertyDefinition`?
self.ctx.helper_call_expr(Helper::ClassPrivateFieldInitSpec, SPAN, arguments, ctx)
}
}
// Static properties
impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
/// Convert static property to initialization expression.
/// Property `static foo = 123;` -> Expression `C.foo = 123` or `_defineProperty(C, "foo", 123)`.
pub(super) fn convert_static_property(
&mut self,
prop: &mut PropertyDefinition<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// Get value, and transform it to replace `this` with reference to class name,
// and transform class property accesses (`object.#prop`)
let value = match &mut prop.value {
Some(value) => {
self.transform_static_initializer(value, ctx);
ctx.ast.move_expression(value)
}
None => ctx.ast.void_0(SPAN),
};
if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key {
self.insert_private_static_init_assignment(ident, value, ctx);
} else {
// Convert to assignment or `_defineProperty` call, depending on `loose` option
let class_binding = if self.is_declaration {
// Class declarations always have a name except `export default class {}`.
// For default export, binding is created when static prop found in 1st pass.
self.class_bindings.name.as_ref().unwrap()
} else {
// Binding is created when static prop found in 1st pass.
self.class_bindings.temp.as_ref().unwrap()
};
let assignee = class_binding.create_read_expression(ctx);
let init_expr = self.create_init_assignment(prop, value, assignee, true, ctx);
self.insert_expr_after_class(init_expr, ctx);
}
}
/// Insert after class:
///
/// Not loose:
/// * Class declaration: `var _prop = {_: value};`
/// * Class expression: `_prop = {_: value}`
///
/// Loose:
/// `Object.defineProperty(Class, _prop, {writable: true, value: value});`
fn insert_private_static_init_assignment(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if self.private_fields_as_properties {
self.insert_private_static_init_assignment_loose(ident, value, ctx);
} else {
self.insert_private_static_init_assignment_not_loose(ident, value, ctx);
}
}
/// Insert after class:
/// `Object.defineProperty(Class, _prop, {writable: true, value: value});`
fn insert_private_static_init_assignment_loose(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// TODO: This logic appears elsewhere. De-duplicate it.
let class_binding = if self.is_declaration {
// Class declarations always have a name except `export default class {}`.
// For default export, binding is created when static prop found in 1st pass.
self.class_bindings.name.as_ref().unwrap()
} else {
// Binding is created when static prop found in 1st pass.
self.class_bindings.temp.as_ref().unwrap()
};
let assignee = class_binding.create_read_expression(ctx);
let assignment = self.create_private_init_assignment_loose(ident, value, assignee, ctx);
self.insert_expr_after_class(assignment, ctx);
}
/// Insert after class:
///
/// * Class declaration: `var _prop = {_: value};`
/// * Class expression: `_prop = {_: value}`
fn insert_private_static_init_assignment_not_loose(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// `_prop = {_: value}`
let property = ctx.ast.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
PropertyKey::StaticIdentifier(ctx.ast.alloc(create_underscore_ident_name(ctx))),
value,
false,
false,
false,
);
let obj = ctx.ast.expression_object(SPAN, ctx.ast.vec1(property), None);
// Insert after class
let private_props = self.private_props_stack.last().unwrap();
let prop = &private_props.props[&ident.name];
if self.is_declaration {
// `var _prop = {_: value};`
let var_decl = create_variable_declaration(&prop.binding, obj, ctx);
self.insert_after_stmts.push(var_decl);
} else {
// `_prop = {_: value}`
let assignment = create_assignment(&prop.binding, obj, ctx);
self.insert_after_exprs.push(assignment);
}
}
}
// Used for both instance and static properties
impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
/// `assignee.foo = value` or `_defineProperty(assignee, "foo", value)`
fn create_init_assignment(
&mut self,
prop: &mut PropertyDefinition<'a>,
value: Expression<'a>,
assignee: Expression<'a>,
is_static: bool,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
if self.set_public_class_fields {
// `assignee.foo = value`
self.create_init_assignment_loose(prop, value, assignee, is_static, ctx)
} else {
// `_defineProperty(assignee, "foo", value)`
self.create_init_assignment_not_loose(prop, value, assignee, ctx)
}
}
/// `this.foo = value` or `_Class.foo = value`
fn create_init_assignment_loose(
&mut self,
prop: &mut PropertyDefinition<'a>,
value: Expression<'a>,
assignee: Expression<'a>,
is_static: bool,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
// In-built static props `name` and `length` need to be set with `_defineProperty`
let needs_define = |name| is_static && (name == "name" || name == "length");
let left = match &mut prop.key {
PropertyKey::StaticIdentifier(ident) => {
if needs_define(&ident.name) {
return self.create_init_assignment_not_loose(prop, value, assignee, ctx);
}
ctx.ast.member_expression_static(SPAN, assignee, ident.as_ref().clone(), false)
}
PropertyKey::StringLiteral(str_lit) if needs_define(&str_lit.value) => {
return self.create_init_assignment_not_loose(prop, value, assignee, ctx);
}
key @ match_expression!(PropertyKey) => {
// TODO: This can also be a numeric key (non-computed). Maybe other key types?
let key = self.create_computed_key_temp_var(key.to_expression_mut(), ctx);
ctx.ast.member_expression_computed(SPAN, assignee, key, false)
}
PropertyKey::PrivateIdentifier(_) => {
// Handled in `convert_instance_property` and `convert_static_property`
unreachable!();
}
};
// TODO: Should this have span of the original `PropertyDefinition`?
ctx.ast.expression_assignment(
SPAN,
AssignmentOperator::Assign,
AssignmentTarget::from(left),
value,
)
}
/// `_defineProperty(this, "foo", value)` or `_defineProperty(_Class, "foo", value)`
fn create_init_assignment_not_loose(
&mut self,
prop: &mut PropertyDefinition<'a>,
value: Expression<'a>,
assignee: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let key = match &mut prop.key {
PropertyKey::StaticIdentifier(ident) => {
ctx.ast.expression_string_literal(ident.span, ident.name.clone(), None)
}
key @ match_expression!(PropertyKey) => {
// TODO: This can also be a numeric key (non-computed). Maybe other key types?
self.create_computed_key_temp_var(key.to_expression_mut(), ctx)
}
PropertyKey::PrivateIdentifier(_) => {
// Handled in `convert_instance_property` and `convert_static_property`
unreachable!();
}
};
let arguments = ctx.ast.vec_from_array([
Argument::from(assignee),
Argument::from(key),
Argument::from(value),
]);
// TODO: Should this have span of the original `PropertyDefinition`?
self.ctx.helper_call_expr(Helper::DefineProperty, SPAN, arguments, ctx)
}
/// `Object.defineProperty(<assignee>, _prop, {writable: true, value: value})`
fn create_private_init_assignment_loose(
&mut self,
ident: &PrivateIdentifier<'a>,
value: Expression<'a>,
assignee: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
// `Object.defineProperty`
let object_symbol_id = ctx.scopes().find_binding(ctx.current_scope_id(), "Object");
let object = ctx.create_ident_expr(
SPAN,
Atom::from("Object"),
object_symbol_id,
ReferenceFlags::Read,
);
let property = ctx.ast.identifier_name(SPAN, "defineProperty");
let callee =
Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false));
// `{writable: true, value: <value>}`
let prop_def = ctx.ast.expression_object(
SPAN,
ctx.ast.vec_from_array([
ctx.ast.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
ctx.ast.property_key_identifier_name(SPAN, Atom::from("writable")),
ctx.ast.expression_boolean_literal(SPAN, true),
false,
false,
false,
),
ctx.ast.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
ctx.ast.property_key_identifier_name(SPAN, Atom::from("value")),
value,
false,
false,
false,
),
]),
None,
);
let private_props = self.private_props_stack.last().unwrap();
let prop = &private_props.props[&ident.name];
let arguments = ctx.ast.vec_from_array([
Argument::from(assignee),
Argument::from(prop.binding.create_read_expression(ctx)),
Argument::from(prop_def),
]);
// TODO: Should this have span of original `PropertyDefinition`?
ctx.ast.expression_call(SPAN, callee, NONE, arguments, false)
}
}

View file

@ -0,0 +1,35 @@
//! ES2022: Class Properties
//! Transform of class static blocks.
use oxc_ast::ast::*;
use oxc_traverse::TraverseCtx;
use super::super::ClassStaticBlock;
use super::ClassProperties;
impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
/// Convert static block to `Expression`.
///
/// `static { x = 1; }` -> `x = 1`
/// `static { x = 1; y = 2; } -> `(() => { x = 1; y = 2; })()`
///
/// TODO: Add tests for this if there aren't any already.
/// Include tests for evaluation order inc that static block goes before class expression
/// unless also static properties, or static block uses class name.
pub(super) fn convert_static_block(
&mut self,
block: &mut StaticBlock<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// TODO: Convert `this` and references to class name.
// `x = class C { static { this.C = C; } }` -> `x = (_C = class C {}, _C.C = _C, _C)`
// TODO: Scope of static block contents becomes outer scope, not scope of class.
// If class expression, assignment in static block moves to a position where it's read from.
// e.g.: `x` here now has read+write `ReferenceFlags`:
// `C = class C { static { x = 1; } }` -> `C = (_C = class C {}, x = 1, _C)`
let expr = ClassStaticBlock::convert_block_to_expression(block, ctx);
self.insert_expr_after_class(expr, ctx);
}
}