-
-
Notifications
You must be signed in to change notification settings - Fork 484
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(transformer): class properties transform
- Loading branch information
1 parent
6284f84
commit 7b2ad83
Showing
10 changed files
with
779 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.