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 29, 2024
1 parent f0c87d4 commit 0eb3ad7
Show file tree
Hide file tree
Showing 10 changed files with 588 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
229 changes: 229 additions & 0 deletions crates/oxc_transformer/src/es2022/class_properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//! 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 };

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));

match constructor {
Some((constructor_index, _)) => {
// 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);
}
None => {
// 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
}
}
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);
}
}
}
12 changes: 11 additions & 1 deletion crates/oxc_transformer/src/es2022/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ use serde::Deserialize;

use crate::env::{can_enable_plugin, Versions};

#[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>,
}

impl ES2022Options {
Expand All @@ -15,6 +18,11 @@ impl ES2022Options {
self
}

pub fn with_class_properties(&mut self, option: Option<ClassPropertiesOptions>) -> &mut Self {
self.class_properties = option;
self
}

#[must_use]
pub fn from_targets_and_bugfixes(targets: Option<&Versions>, bugfixes: bool) -> Self {
Self {
Expand All @@ -23,6 +31,8 @@ impl ES2022Options {
targets,
bugfixes,
),
class_properties: can_enable_plugin("transform-class-properties", targets, bugfixes)
.then(Default::default),
}
}
}
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
7 changes: 5 additions & 2 deletions crates/oxc_transformer/src/options/transformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
es2019::ES2019Options,
es2020::ES2020Options,
es2021::ES2021Options,
es2022::ES2022Options,
es2022::{ClassPropertiesOptions, ES2022Options},
jsx::JsxOptions,
options::babel::BabelOptions,
regexp::RegExpOptions,
Expand Down Expand Up @@ -102,7 +102,10 @@ impl TransformOptions {
es2019: ES2019Options { optional_catch_binding: true },
es2020: ES2020Options { nullish_coalescing_operator: true },
es2021: ES2021Options { logical_assignment_operators: true },
es2022: ES2022Options { class_static_block: true },
es2022: ES2022Options {
class_static_block: true,
class_properties: Some(ClassPropertiesOptions::default()),
},
helper_loader: HelperLoaderOptions {
mode: HelperLoaderMode::Runtime,
..Default::default()
Expand Down
Loading

0 comments on commit 0eb3ad7

Please sign in to comment.