Skip to content

Commit

Permalink
feat(transformer/class-properties): support `private_fields_as_proper…
Browse files Browse the repository at this point in the history
…ties` assumption
  • Loading branch information
overlookmotel committed Dec 8, 2024
1 parent 3bae741 commit 846382a
Show file tree
Hide file tree
Showing 13 changed files with 441 additions and 183 deletions.
13 changes: 13 additions & 0 deletions crates/oxc_ast/src/ast_builder_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ impl<'a> AstBuilder<'a> {
mem::replace(target, dummy.into())
}

/// Moves the assignment target out by replacing it with a dummy target with
/// no name and an empty [`Span`].
// TODO: Delete this if not using it.
#[inline]
pub fn move_simple_assignment_target(
self,
target: &mut SimpleAssignmentTarget<'a>,
) -> SimpleAssignmentTarget<'a> {
let dummy =
self.simple_assignment_target_identifier_reference(Span::default(), Atom::from(""));
mem::replace(target, dummy)
}

/// Move a declaration out by replacing it with an empty [variable
/// declaration](Declaration::VariableDeclaration).
#[inline]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_transformer/src/common/helper_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ pub enum Helper {
ClassPrivateFieldSet2,
AssertClassBrand,
ToSetter,
ClassPrivateFieldLooseKey,
ClassPrivateFieldLooseBase,
}

impl Helper {
Expand All @@ -174,6 +176,8 @@ impl Helper {
Self::ClassPrivateFieldSet2 => "classPrivateFieldSet2",
Self::AssertClassBrand => "assertClassBrand",
Self::ToSetter => "toSetter",
Self::ClassPrivateFieldLooseKey => "classPrivateFieldLooseKey",
Self::ClassPrivateFieldLooseBase => "classPrivateFieldLooseBase",
}
}
}
Expand Down
1 change: 0 additions & 1 deletion crates/oxc_transformer/src/compiler_assumptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ pub struct CompilerAssumptions {
pub private_fields_as_symbols: bool,

#[serde(default)]
#[deprecated = "Not Implemented"]
pub private_fields_as_properties: bool,

#[serde(default)]
Expand Down
232 changes: 196 additions & 36 deletions crates/oxc_transformer/src/es2022/class_properties/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,29 +104,40 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {

let mut exprs = ctx.ast.vec_with_capacity(expr_count);

// Insert `_prop = new WeakMap()` expressions for private instance props.
// Insert `_prop = new WeakMap()` expressions for private instance props
// (or `_prop = _classPrivateFieldLooseKey("prop")` if loose mode).
// Babel has these always go first, regardless of order of class elements.
// Also insert `var _prop;` temp var declarations for private static props.
let private_props = self.private_props_stack.last();
if let Some(private_props) = private_props {
let mut weakmap_symbol_id = None;
exprs.extend(private_props.props.values().filter_map(|prop| {
// Insert `var _prop;` declaration.
// Do it here rather than when binding was created to maintain same order of `var`
// declarations as Babel. `c = class C { #x = 1; static y = 2; }` -> `var _C, _x;`
self.ctx.var_declarations.insert_var(&prop.binding, ctx);

if prop.is_static {
return None;
}
// Insert `var _prop;` declarations here rather than when binding was created to maintain
// same order of `var` declarations as Babel.
// `c = class C { #x = 1; static y = 2; }` -> `var _C, _x;`
// TODO(improve-on-babel): Simplify this.
if self.private_fields_as_properties {
exprs.extend(private_props.props.iter().map(|(name, prop)| {
// Insert `var _prop;` declaration
self.ctx.var_declarations.insert_var(&prop.binding, ctx);

// `_prop = _classPrivateFieldLooseKey("prop")`
let value = self.create_private_prop_key_loose(name, ctx);
create_assignment(&prop.binding, value, ctx)
}));
} else {
let mut weakmap_symbol_id = None;
exprs.extend(private_props.props.values().filter_map(|prop| {
// Insert `var _prop;` declaration
self.ctx.var_declarations.insert_var(&prop.binding, ctx);

// `_prop = new WeakMap()`
Some(create_assignment(
&prop.binding,
create_new_weakmap(&mut weakmap_symbol_id, ctx),
ctx,
))
}));
if prop.is_static {
return None;
}

// `_prop = new WeakMap()`
let value = create_new_weakmap(&mut weakmap_symbol_id, ctx);
Some(create_assignment(&prop.binding, value, ctx))
}));
}
}

// Insert computed key initializers
Expand Down Expand Up @@ -216,23 +227,31 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
}

if let Some(private_props) = self.private_props_stack.last() {
// TODO: Only call `insert_many_before` if some private *instance* props
let mut weakmap_symbol_id = None;
self.ctx.statement_injector.insert_many_before(
&stmt_address,
private_props.props.values().filter_map(|prop| {
if prop.is_static {
return None;
}

// `var _prop = new WeakMap()`
Some(create_variable_declaration(
&prop.binding,
create_new_weakmap(&mut weakmap_symbol_id, ctx),
ctx,
))
}),
);
if self.private_fields_as_properties {
self.ctx.statement_injector.insert_many_before(
&stmt_address,
private_props.props.iter().map(|(name, prop)| {
// `var _prop = _classPrivateFieldLooseKey("prop");`
let value = self.create_private_prop_key_loose(name, ctx);
create_variable_declaration(&prop.binding, value, ctx)
}),
);
} else {
// TODO: Only call `insert_many_before` if some private *instance* props
let mut weakmap_symbol_id = None;
self.ctx.statement_injector.insert_many_before(
&stmt_address,
private_props.props.values().filter_map(|prop| {
if prop.is_static {
return None;
}

// `var _prop = new WeakMap();`
let value = create_new_weakmap(&mut weakmap_symbol_id, ctx);
Some(create_variable_declaration(&prop.binding, value, ctx))
}),
);
}
}

if !self.insert_after_stmts.is_empty() {
Expand All @@ -242,6 +261,24 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
}
}

/// `_classPrivateFieldLooseKey("prop")`
fn create_private_prop_key_loose(
&self,
name: &Atom<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
self.ctx.helper_call_expr(
Helper::ClassPrivateFieldLooseKey,
SPAN,
ctx.ast.vec1(Argument::from(ctx.ast.expression_string_literal(
SPAN,
name.clone(),
None,
))),
ctx,
)
}

/// Main guts of the transform.
fn transform_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) {
// TODO(improve-on-babel): If outer scope is sloppy mode, all code which is moved to outside
Expand Down Expand Up @@ -600,12 +637,87 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
self.ctx.helper_call_expr(Helper::DefineProperty, SPAN, arguments, ctx)
}

/// Create `_classPrivateFieldInitSpec(this, _prop, value)` to be inserted into class constructor.
/// 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];
Expand All @@ -619,13 +731,58 @@ impl<'a, 'ctx> ClassProperties<'a, '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(
Expand Down Expand Up @@ -791,6 +948,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
/// * `None` = Not looked up yet.
/// * `Some(None)` = Has been looked up, and `WeakMap` is unbound.
/// * `Some(Some(symbol_id))` = Has been looked up, and `WeakMap` has a local binding.
///
/// This is an optimization to avoid looking up the symbol for `WeakMap` over and over when defining
/// multiple private properties.
#[expect(clippy::option_option)]
fn create_new_weakmap<'a>(
symbol_id: &mut Option<Option<SymbolId>>,
Expand Down
6 changes: 6 additions & 0 deletions crates/oxc_transformer/src/es2022/class_properties/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ pub struct ClassProperties<'a, 'ctx> {
//
/// If `true`, set properties with `=`, instead of `_defineProperty` helper.
set_public_class_fields: bool,
/// If `true`, record private properties as string keys
private_fields_as_properties: bool,
/// If `true`, transform static blocks.
transform_static_blocks: bool,

Expand Down Expand Up @@ -227,9 +229,13 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
) -> Self {
// TODO: Raise error if these 2 options are inconsistent
let set_public_class_fields = options.loose || ctx.assumptions.set_public_class_fields;
// TODO: Raise error if these 2 options are inconsistent
let private_fields_as_properties =
options.loose || ctx.assumptions.private_fields_as_properties;

Self {
set_public_class_fields,
private_fields_as_properties,
transform_static_blocks,
ctx,
private_props_stack: PrivatePropsStack::default(),
Expand Down
Loading

0 comments on commit 846382a

Please sign in to comment.