Skip to content

Commit

Permalink
fix(transformer/class-properties): create temp var for class where re…
Browse files Browse the repository at this point in the history
…quired
  • Loading branch information
overlookmotel committed Dec 2, 2024
1 parent 3547117 commit 29fed4e
Show file tree
Hide file tree
Showing 8 changed files with 555 additions and 456 deletions.
154 changes: 93 additions & 61 deletions crates/oxc_transformer/src/es2022/class_properties/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ use oxc_traverse::{BoundIdentifier, TraverseCtx};

use crate::common::helper_loader::Helper;

use super::super::ClassStaticBlock;
use super::{super::ClassStaticBlock, ClassBindings};
use super::{
private_props::{PrivateProp, PrivateProps},
utils::{
create_assignment, create_underscore_ident_name, create_variable_declaration,
exprs_into_stmts,
},
ClassName, ClassProperties, FxIndexMap,
ClassProperties, FxIndexMap,
};

impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
Expand Down Expand Up @@ -58,10 +58,6 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
return 0;
}

self.class_name = ClassName::Name(match &class.id {
Some(id) => id.name.as_str(),
None => "Class",
});
self.is_declaration = false;

self.transform_class(class, ctx);
Expand Down Expand Up @@ -104,10 +100,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
// TODO: Deduct static private props from `expr_count`.
// Or maybe should store count and increment it when create private static props?
// They're probably pretty rare, so it'll be rarely used.
expr_count += match &self.class_name {
ClassName::Binding(_) => 2,
ClassName::Name(_) => 1,
};
expr_count += 1 + usize::from(self.class_bindings.temp.is_some());

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

Expand Down Expand Up @@ -141,7 +134,11 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {

// Insert class + static property assignments + static blocks
let class_expr = ctx.ast.move_expression(expr);
if let ClassName::Binding(binding) = &self.class_name {
if let Some(binding) = &self.class_bindings.temp {
if !self.temp_var_is_created {
self.ctx.var_declarations.insert_var(binding, None, ctx);
}

// `_Class = class {}`
let assignment = create_assignment(binding, class_expr, ctx);
exprs.push(assignment);
Expand Down Expand Up @@ -179,40 +176,21 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
return;
}

// Class declarations are always named, except for `export default class {}`, which is handled separately
let ident = class.id.as_ref().unwrap();
self.class_name = ClassName::Binding(BoundIdentifier::from_binding_ident(ident));

self.transform_class_declaration_impl(class, stmt_address, ctx);
}

/// Transform `export default class {}`.
///
/// Separate function as this is only circumstance where have to deal with anonymous class declaration,
/// and it's an uncommon case (can only be 1 per file).
// TODO: This method is now defunct. Can just have 1 `transform_class_declaration` function.
pub(super) fn transform_class_export_default(
&mut self,
class: &mut Class<'a>,
stmt_address: Address,
ctx: &mut TraverseCtx<'a>,
) {
// Class declarations as default export may not have a name
self.class_name = match class.id.as_ref() {
Some(ident) => ClassName::Binding(BoundIdentifier::from_binding_ident(ident)),
None => ClassName::Name("Class"),
};

self.transform_class_declaration_impl(class, stmt_address, ctx);

// If class was unnamed `export default class {}`, and a binding is required, set its name.
// e.g. `export default class { static x = 1; }` -> `export default class _Class {}; _Class.x = 1;`
// TODO(improve-on-babel): Could avoid this if treated `export default class {}` as a class expression
// instead of a class declaration.
if class.id.is_none() {
if let ClassName::Binding(binding) = &self.class_name {
class.id = Some(binding.create_binding_identifier(ctx));
}
}
}

fn transform_class_declaration_impl(
Expand All @@ -227,6 +205,33 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {

// TODO: Run other transforms on inserted statements. How?

if let Some(temp_binding) = &self.class_bindings.temp {
// Binding for class name is required
if let Some(ident) = &class.id {
// Insert `_Class = Class` after class.
// TODO(improve-on-babel): Could just insert `var _Class = Class;` after class,
// rather than separate `var _Class` declaration.
if !self.temp_var_is_created {
self.ctx.var_declarations.insert_var(temp_binding, None, ctx);
}

let class_name = ctx.create_bound_ident_expr(
SPAN,
ident.name.clone(),
ident.symbol_id(),
ReferenceFlags::Read,
);
let expr = create_assignment(temp_binding, class_name, ctx);
let stmt = ctx.ast.statement_expression(SPAN, expr);
self.insert_after_stmts.insert(0, stmt);
} else {
// Class must be default export `export default class {}`, as all other class declarations
// always have a name. Set class name.
*ctx.symbols_mut().get_flags_mut(temp_binding.symbol_id) = SymbolFlags::Class;
class.id = Some(temp_binding.create_binding_identifier(ctx));
}
}

// Insert expressions before/after class
if !self.insert_before.is_empty() {
self.ctx.statement_injector.insert_many_before(
Expand Down Expand Up @@ -343,24 +348,42 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
return;
}

// Create temp var if class has any static props
if has_static_prop {
// TODO(improve-on-babel): Even though private static properties may not access
// class name, Babel still creates a temp var for class. That's unnecessary.
self.initialize_class_name_binding(ctx);
// Initialize class binding vars
self.class_bindings = ClassBindings {
name: class.id.as_ref().map(BoundIdentifier::from_binding_ident),
temp: None,
};

// Static prop in class expression or anonymous `export default class {}`
// always requires a temp var for class. Static prop in class declaration doesn't.
// TODO(improve-on-babel): Inserting the temp var `var _Class` statement here is only necessary
// to match Babel's output. It'd be simpler just to insert it at the end and get rid of
// `temp_var_is_created` that tracks whether it's done already or not.
let need_temp_var = has_static_prop && (!self.is_declaration || class.id.is_none());
self.temp_var_is_created = need_temp_var;

if need_temp_var {
let temp_binding = self.class_bindings.get_or_init_temp_binding(ctx);
if self.is_declaration && class.id.is_none() {
// Anonymous `export default class {}`, set class name binding to temp var.
// Actual class name will be set to this later.
self.class_bindings.name = Some(temp_binding.clone());
} else {
// Create temp var `var _Class;` statement
self.ctx.var_declarations.insert_var(temp_binding, None, ctx);
}
}

// Add entry to `private_props_stack`
if private_props.is_empty() {
self.private_props_stack.push(None);
} else {
let class_binding = match &self.class_name {
ClassName::Binding(binding) => Some(binding.clone()),
ClassName::Name(_) => None,
};
self.private_props_stack.push(Some(PrivateProps {
props: private_props,
class_binding,
// Use temp binding while transpiling static property initializers.
// Will be set to class name binding at end of this function, before class body
// is visited.
class_bindings: self.class_bindings.clone(),
is_declaration: self.is_declaration,
}));
}
Expand Down Expand Up @@ -407,6 +430,27 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
Self::insert_constructor(class, instance_inits, ctx);
}
}

if let Some(private_props) = self.private_props_stack.last_mut() {
// Transfer state of `temp` binding from `private_props_stack` to `self`.
// A temp binding may have been created while transpiling private fields in
// static prop initializers.
// TODO: Do this where `class_bindings.temp` is consumed instead?
if let Some(temp_binding) = &private_props.class_bindings.temp {
self.class_bindings.temp = Some(temp_binding.clone());
}

// Static private fields reference class name (not temp var) in class declarations.
// `class Class { static #prop; method() { return obj.#prop; } }`
// -> `method() { return _assertClassBrand(Class, obj, _prop)._; }`
// (note `Class` in `_assertClassBrand(Class, ...)`, not `_Class`)
// So set "temp" binding to actual class name while visiting class body.
// Note: If declaration is `export default class {}` with no name, and class has static props,
// then class has had name binding created already. So name binding is always `Some`.
if self.is_declaration {
private_props.class_bindings.temp = private_props.class_bindings.name.clone();
}
}
}

/// Pop from private props stack.
Expand Down Expand Up @@ -467,33 +511,21 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
self.insert_private_static_init_assignment(ident, value, ctx);
} else {
// Convert to assignment or `_defineProperty` call, depending on `loose` option
let ClassName::Binding(class_binding) = &self.class_name else {
// Binding is initialized in 1st pass in `transform_class` when a static prop is found
unreachable!();
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);
}
}

/// Create a binding for class name, if there isn't one already.
fn initialize_class_name_binding(&mut self, ctx: &mut TraverseCtx<'a>) -> &BoundIdentifier<'a> {
if let ClassName::Name(name) = &self.class_name {
let binding = if self.is_declaration {
ctx.generate_uid_in_current_scope(name, SymbolFlags::Class)
} else {
let flags = SymbolFlags::FunctionScopedVariable;
let binding = ctx.generate_uid_in_current_scope(name, flags);
self.ctx.var_declarations.insert_var(&binding, None, ctx);
binding
};
self.class_name = ClassName::Binding(binding);
}
let ClassName::Binding(binding) = &self.class_name else { unreachable!() };
binding
}

/// `assignee.foo = value` or `_defineProperty(assignee, "foo", value)`
fn create_init_assignment(
&mut self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use oxc_syntax::symbol::{SymbolFlags, SymbolId};
use oxc_traverse::{BoundIdentifier, TraverseCtx};

#[derive(Default, Clone)]
pub(super) struct ClassBindings<'a> {
/// Binding for class name, if class has name
pub name: Option<BoundIdentifier<'a>>,
/// Temp var for class.
/// e.g. `_Class` in `_Class = class {}, _Class.x = 1, _Class`
pub temp: Option<BoundIdentifier<'a>>,
}

impl<'a> ClassBindings<'a> {
/// Get `SymbolId` for name binding.
pub fn name_symbol_id(&self) -> Option<SymbolId> {
self.name.as_ref().map(|binding| binding.symbol_id)
}

/// Create a binding for temp var, if there isn't one already.
pub fn get_or_init_temp_binding(&mut self, ctx: &mut TraverseCtx<'a>) -> &BoundIdentifier<'a> {
self.temp.get_or_insert_with(|| {
// Base temp binding name on class name, or "Class" if no name.
// TODO(improve-on-babel): If class name var isn't mutated, no need for temp var for
// class declaration. Can just use class binding.
let name = self.name.as_ref().map_or("Class", |binding| binding.name.as_str());
ctx.generate_uid_in_current_scope(name, SymbolFlags::FunctionScopedVariable)
})
}
}
27 changes: 11 additions & 16 deletions crates/oxc_transformer/src/es2022/class_properties/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,13 @@
//!
//! Implementation is split into several files:
//!
//! * `mod.rs`: Setup, visitor, and ancillary types.
//! * `mod.rs`: Setup and visitor.
//! * `class.rs`: Transform of class body.
//! * `constructor.rs`: Insertion of property initializers into class constructor.
//! * `private.rs`: Transform of private property usages (`this.#prop`).
//! * `private_props.rs`: Structures storing details of private properties.
//! * `static_prop.rs`: Transform of static property initializers.
//! * `class_bindings.rs`: Structure containing bindings for class name and temp var.
//! * `utils.rs`: Utility functions.
//!
//! ## References
Expand All @@ -148,16 +149,18 @@ use serde::Deserialize;
use oxc_allocator::{Address, GetAddress};
use oxc_ast::ast::*;
use oxc_data_structures::stack::NonEmptyStack;
use oxc_traverse::{BoundIdentifier, Traverse, TraverseCtx};
use oxc_traverse::{Traverse, TraverseCtx};

use crate::TransformCtx;

mod class;
mod class_bindings;
mod constructor;
mod private;
mod private_props;
mod static_prop;
mod utils;
use class_bindings::ClassBindings;
use private_props::PrivatePropsStack;

type FxIndexMap<K, V> = IndexMap<K, V, FxBuildHasher>;
Expand Down Expand Up @@ -205,10 +208,10 @@ pub struct ClassProperties<'a, 'ctx> {
//
/// `true` for class declaration, `false` for class expression
is_declaration: bool,
/// Var for class.
/// e.g. `X` in `class X {}`.
/// e.g. `_Class` in `_Class = class {}, _Class.x = 1, _Class`
class_name: ClassName<'a>,
/// Bindings for class name and temp var for class
class_bindings: ClassBindings<'a>,
/// `true` if temp var for class has been inserted
temp_var_is_created: bool,
/// Expressions to insert before class
insert_before: Vec<Expression<'a>>,
/// Expressions to insert after class expression
Expand All @@ -217,15 +220,6 @@ pub struct ClassProperties<'a, 'ctx> {
insert_after_stmts: Vec<Statement<'a>>,
}

/// Representation of binding for class name.
enum ClassName<'a> {
/// Class has a name. This is the binding.
Binding(BoundIdentifier<'a>),
/// Class is anonymous.
/// This is the name it would have if we need to set class name, in order to reference it.
Name(&'a str),
}

impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
pub fn new(
options: ClassPropertiesOptions,
Expand All @@ -244,7 +238,8 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
class_expression_addresses_stack: NonEmptyStack::new(Address::DUMMY),
// Temporary values - overwritten when entering class
is_declaration: false,
class_name: ClassName::Name(""),
class_bindings: ClassBindings::default(),
temp_var_is_created: false,
// `Vec`s and `FxHashMap`s which are reused for every class being transformed
insert_before: vec![],
insert_after_exprs: vec![],
Expand Down
Loading

0 comments on commit 29fed4e

Please sign in to comment.