Skip to content

Commit

Permalink
feat(transformer): class properties transform
Browse files Browse the repository at this point in the history
  • Loading branch information
overlookmotel committed Oct 30, 2024
1 parent 6284f84 commit 7b2ad83
Show file tree
Hide file tree
Showing 10 changed files with 779 additions and 15 deletions.
8 changes: 8 additions & 0 deletions crates/oxc_ast/src/ast_builder_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ impl<'a> AstBuilder<'a> {
mem::replace(element, empty_element)
}

/// Move a class element out by replacing it with an empty
/// [StaticBlock](ClassElement::StaticBlock).
// TODO: Delete this method if not using it.
pub fn move_class_element(self, element: &mut ClassElement<'a>) -> ClassElement<'a> {
let empty_element = self.class_element_static_block(Span::default(), self.vec());
mem::replace(element, empty_element)
}

/// Take the contents of a arena-allocated [`Vec`], leaving an empty vec in
/// its place. This is akin to [`std::mem::take`].
#[inline]
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_transformer/src/common/helper_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,15 @@ fn default_as_module_name() -> Cow<'static, str> {
pub enum Helper {
AsyncToGenerator,
ObjectSpread2,
DefineProperty,
}

impl Helper {
const fn name(self) -> &'static str {
match self {
Self::AsyncToGenerator => "asyncToGenerator",
Self::ObjectSpread2 => "objectSpread2",
Self::DefineProperty => "defineProperty",
}
}
}
Expand Down
276 changes: 276 additions & 0 deletions crates/oxc_transformer/src/es2022/class_properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
//! ES2022: Class Properties
//!
//! This plugin transforms class properties to initializers inside class constructor.
//!
//! > This plugin is included in `preset-env`, in ES2022
//!
//! ## Example
//!
//! Input:
//! ```js
//! class C {
//! foo = 123;
//! #bar = 456;
//! }
//!
//! let x = 123;
//! class D extends S {
//! foo = x;
//! constructor(x) {
//! if (x) {
//! let s = super(x);
//! } else {
//! super(x);
//! }
//! }
//! }
//! ```
//!
//! Output:
//! ```js
//! var _bar = /*#__PURE__*/ new WeakMap();
//! class C {
//! constructor() {
//! babelHelpers.defineProperty(this, "foo", 123);
//! babelHelpers.classPrivateFieldInitSpec(this, _bar, 456);
//! }
//! }
//!
//! let x = 123;
//! class D extends S {
//! constructor(_x) {
//! if (_x) {
//! let s = (super(_x), babelHelpers.defineProperty(this, "foo", x));
//! } else {
//! super(_x);
//! babelHelpers.defineProperty(this, "foo", x);
//! }
//! }
//! }
//! ```
//!
//! ## Implementation
//!
//! Implementation based on [@babel/plugin-transform-class-properties](https://babel.dev/docs/babel-plugin-transform-class-properties).
//!
//! ## References:
//! * Babel plugin implementation:
//! * <https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-class-properties>
//! * <https://github.com/babel/babel/blob/main/packages/babel-helper-create-class-features-plugin/src/index.ts>
//! * <https://github.com/babel/babel/blob/main/packages/babel-helper-create-class-features-plugin/src/fields.ts>
//! * Class properties TC39 proposal: <https://github.com/tc39/proposal-class-fields>
use serde::Deserialize;

use oxc_ast::{ast::*, AstBuilder, NONE};
use oxc_span::SPAN;
use oxc_syntax::scope::ScopeFlags;
use oxc_traverse::{Ancestor, Traverse, TraverseCtx};

use crate::{common::helper_loader::Helper, TransformCtx};

#[derive(Debug, Default, Clone, Copy, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ClassPropertiesOptions {
#[serde(alias = "loose")]
pub(crate) set_public_class_fields: bool,
}

pub struct ClassProperties<'a, 'ctx> {
#[expect(dead_code)]
options: ClassPropertiesOptions,
ctx: &'ctx TransformCtx<'a>,
}

impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
pub fn new(options: ClassPropertiesOptions, ctx: &'ctx TransformCtx<'a>) -> Self {
Self { options, ctx }
}
}

impl<'a, 'ctx> Traverse<'a> for ClassProperties<'a, 'ctx> {
fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
// Check if class has any properties and get index and `ScopeId` of constructor (if class has one)
let mut instance_prop_count = 0;
let mut static_props_count = 0;
let mut constructor = None;
for (index, element) in body.body.iter().enumerate() {
match element {
ClassElement::PropertyDefinition(prop) => {
if !prop.decorators.is_empty()
|| prop.r#type == PropertyDefinitionType::TSAbstractPropertyDefinition
{
// TODO: Raise error
return;
}

if prop.r#static {
static_props_count += 1;
} else {
instance_prop_count += 1;
}
}
ClassElement::MethodDefinition(method) => {
if method.kind == MethodDefinitionKind::Constructor {
if method.value.body.is_none() {
// Constructor has no body. TODO: Don't bail out here.
return;
}

// Record index constructor has after properties before it are removed
let index = index - static_props_count - instance_prop_count;
constructor = Some((index, method.value.scope_id.get().unwrap()));
}
}
_ => {}
};
}

if instance_prop_count == 0 && static_props_count == 0 {
return;
}

// Extract properties from class body
let mut instance_inits = Vec::with_capacity(instance_prop_count);
// let mut static_props = Vec::with_capacity(static_props_count);
body.body.retain_mut(|element| {
let ClassElement::PropertyDefinition(prop) = element else { return true };

#[expect(clippy::redundant_else)]
if prop.r#static {
// TODO
return false;
} else {
// TODO: Handle `loose` option
let key = match &prop.key {
PropertyKey::StaticIdentifier(ident) => {
ctx.ast.expression_string_literal(ident.span, ident.name.clone())
}
_ => {
// TODO: Handle private properties
// TODO: Handle computed property key
ctx.ast.expression_string_literal(SPAN, Atom::from("oops"))
}
};
let value = match &mut prop.value {
Some(value) => ctx.ast.move_expression(value),
None => ctx.ast.void_0(SPAN),
};
let args = ctx.ast.vec_from_iter(
[ctx.ast.expression_this(SPAN), key, value].into_iter().map(Argument::from),
);
// TODO: Should this have span of the original `PropertyDefinition`?
let expr = self.ctx.helper_call_expr(Helper::DefineProperty, args, ctx);
instance_inits.push(expr);
}

false
});

// Insert instance initializers into constructor
if !instance_inits.is_empty() {
// TODO: Re-parent any scopes within initializers.
let has_super_class = match ctx.parent() {
Ancestor::ClassBody(class) => class.super_class().is_some(),
_ => unreachable!(),
};

if let Some((constructor_index, _)) = constructor {
// Existing constructor - amend it
Self::insert_inits_into_constructor(body, instance_inits, constructor_index, ctx);
} else {
// No constructor - create one
Self::insert_constructor(body, instance_inits, has_super_class, ctx);
}
}

// TODO: Static properties
}
}

impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
fn insert_inits_into_constructor(
body: &mut ClassBody<'a>,
inits: Vec<Expression<'a>>,
constructor_index: usize,
ctx: &mut TraverseCtx<'a>,
) {
// TODO: Insert after `super()` if class has super-class.
// TODO: Insert as expression sequence if `super()` is used in an expression.
// TODO: Handle where vars used in property init clash with vars in top scope of constructor.
let element = body.body.get_mut(constructor_index).unwrap();
let ClassElement::MethodDefinition(method) = element else { unreachable!() };
let func_body = method.value.body.as_mut().unwrap();
func_body
.statements
.splice(0..0, inits.into_iter().map(|expr| ctx.ast.statement_expression(SPAN, expr)));
}

fn insert_constructor(
body: &mut ClassBody<'a>,
inits: Vec<Expression<'a>>,
has_super_class: bool,
ctx: &mut TraverseCtx<'a>,
) {
let inits_count = inits.len();
// TODO: Should these have the span of the original `PropertyDefinition`s?
let init_stmts = inits.into_iter().map(|expr| ctx.ast.statement_expression(SPAN, expr));

// Add `super();` statement if class has a super class
let stmts = if has_super_class {
let mut stmts = ctx.ast.vec_with_capacity(inits_count + 1);
stmts.push(Self::create_super_call_stmt(ctx.ast));
stmts.extend(init_stmts);
stmts
} else {
ctx.ast.vec_from_iter(init_stmts)
};

let scope_id = ctx.create_child_scope_of_current(ScopeFlags::Function);

let ctor = ClassElement::MethodDefinition(ctx.ast.alloc_method_definition(
MethodDefinitionType::MethodDefinition,
SPAN,
ctx.ast.vec(),
PropertyKey::StaticIdentifier(
ctx.ast.alloc_identifier_name(SPAN, Atom::from("constructor")),
),
ctx.ast.alloc_function_with_scope_id(
FunctionType::FunctionExpression,
SPAN,
None,
false,
false,
false,
NONE,
NONE,
ctx.ast.alloc_formal_parameters(
SPAN,
FormalParameterKind::FormalParameter,
ctx.ast.vec(),
NONE,
),
NONE,
Some(ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), stmts)),
scope_id,
),
MethodDefinitionKind::Constructor,
false,
false,
false,
false,
None,
));

// TODO(improve-on-babel): Could push constructor onto end of elements, instead of inserting as first
body.body.insert(0, ctor);
}

/// `super();`
fn create_super_call_stmt(ast: AstBuilder<'a>) -> Statement<'a> {
ast.statement_expression(
SPAN,
ast.expression_call(SPAN, ast.expression_super(SPAN), NONE, ast.vec(), false),
)
}
}
25 changes: 20 additions & 5 deletions crates/oxc_transformer/src/es2022/mod.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
use oxc_ast::ast::*;
use oxc_traverse::{Traverse, TraverseCtx};

use crate::TransformCtx;

mod class_properties;
mod class_static_block;
mod options;

use class_properties::ClassProperties;
pub use class_properties::ClassPropertiesOptions;
use class_static_block::ClassStaticBlock;

pub use options::ES2022Options;

pub struct ES2022 {
pub struct ES2022<'a, 'ctx> {
options: ES2022Options,
// Plugins
class_static_block: ClassStaticBlock,
class_properties: Option<ClassProperties<'a, 'ctx>>,
}

impl ES2022 {
pub fn new(options: ES2022Options) -> Self {
Self { options, class_static_block: ClassStaticBlock::new() }
impl<'a, 'ctx> ES2022<'a, 'ctx> {
pub fn new(options: ES2022Options, ctx: &'ctx TransformCtx<'a>) -> Self {
Self {
options,
class_static_block: ClassStaticBlock::new(),
class_properties: options
.class_properties
.map(|options| ClassProperties::new(options, ctx)),
}
}
}

impl<'a> Traverse<'a> for ES2022 {
impl<'a, 'ctx> Traverse<'a> for ES2022<'a, 'ctx> {
fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
if self.options.class_static_block {
self.class_static_block.enter_class_body(body, ctx);
}
if let Some(class_properties) = &mut self.class_properties {
class_properties.enter_class_body(body, ctx);
}
}
}
5 changes: 4 additions & 1 deletion crates/oxc_transformer/src/es2022/options.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use serde::Deserialize;

#[derive(Debug, Default, Clone, Deserialize)]
use super::ClassPropertiesOptions;

#[derive(Debug, Default, Clone, Copy, Deserialize)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct ES2022Options {
#[serde(skip)]
pub class_static_block: bool,
pub class_properties: Option<ClassPropertiesOptions>,
}
4 changes: 2 additions & 2 deletions crates/oxc_transformer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ impl<'a> Transformer<'a> {
.is_typescript()
.then(|| TypeScript::new(&self.options.typescript, &self.ctx)),
x1_jsx: Jsx::new(self.options.jsx, ast_builder, &self.ctx),
x2_es2022: ES2022::new(self.options.es2022),
x2_es2022: ES2022::new(self.options.es2022, &self.ctx),
x2_es2021: ES2021::new(self.options.es2021, &self.ctx),
x2_es2020: ES2020::new(self.options.es2020, &self.ctx),
x2_es2019: ES2019::new(self.options.es2019),
Expand All @@ -118,7 +118,7 @@ struct TransformerImpl<'a, 'ctx> {
// NOTE: all callbacks must run in order.
x0_typescript: Option<TypeScript<'a, 'ctx>>,
x1_jsx: Jsx<'a, 'ctx>,
x2_es2022: ES2022,
x2_es2022: ES2022<'a, 'ctx>,
x2_es2021: ES2021<'a, 'ctx>,
x2_es2020: ES2020<'a, 'ctx>,
x2_es2019: ES2019,
Expand Down
Loading

0 comments on commit 7b2ad83

Please sign in to comment.