-
-
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
f0c87d4
commit 906e8fd
Showing
10 changed files
with
586 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,227 @@ | ||
//! 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::*, NONE}; | ||
use oxc_span::SPAN; | ||
use oxc_traverse::{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)] | ||
loose: bool, | ||
ctx: &'ctx TransformCtx<'a>, | ||
} | ||
|
||
impl<'a, 'ctx> ClassProperties<'a, 'ctx> { | ||
pub fn new(options: ClassPropertiesOptions, ctx: &'ctx TransformCtx<'a>) -> Self { | ||
Self { loose: options.set_public_class_fields, 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), | ||
); | ||
let expr = self.ctx.helper_call_expr(Helper::DefineProperty, args, ctx); | ||
instance_inits.push(expr); | ||
} | ||
|
||
false | ||
}); | ||
|
||
// Insert instance initializers into constructor | ||
// TODO: Re-parent any scopes within initializers. | ||
let instance_init_stmts = | ||
instance_inits.into_iter().map(|expr| ctx.ast.statement_expression(SPAN, expr)); | ||
|
||
if let Some((constructor_index, _)) = constructor { | ||
// 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, instance_init_stmts); | ||
} else { | ||
// No constructor - insert one | ||
// TODO: Add `super()` if class has super-class. | ||
let method = ctx.ast.alloc_method_definition( | ||
MethodDefinitionType::MethodDefinition, | ||
SPAN, | ||
ctx.ast.vec(), | ||
PropertyKey::StaticIdentifier( | ||
ctx.ast.alloc_identifier_name(SPAN, Atom::from("constructor")), | ||
), | ||
// TODO: Create `ScopeId` for function | ||
ctx.ast.alloc_function( | ||
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(), | ||
ctx.ast.vec_from_iter(instance_init_stmts), | ||
)), | ||
), | ||
MethodDefinitionKind::Constructor, | ||
false, | ||
false, | ||
false, | ||
false, | ||
None, | ||
); | ||
let method = ClassElement::MethodDefinition(method); | ||
body.body.insert(0, method); | ||
} | ||
|
||
// TODO: Static properties | ||
} | ||
} |
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
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
Oops, something went wrong.