diff --git a/crates/oxc_ast/src/ast_builder_impl.rs b/crates/oxc_ast/src/ast_builder_impl.rs index 10aafab345c5e7..a02c7ccb7b46f2 100644 --- a/crates/oxc_ast/src/ast_builder_impl.rs +++ b/crates/oxc_ast/src/ast_builder_impl.rs @@ -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] diff --git a/crates/oxc_transformer/src/common/helper_loader.rs b/crates/oxc_transformer/src/common/helper_loader.rs index 418b32c3f851b0..bc899cbddb6903 100644 --- a/crates/oxc_transformer/src/common/helper_loader.rs +++ b/crates/oxc_transformer/src/common/helper_loader.rs @@ -139,6 +139,7 @@ fn default_as_module_name() -> Cow<'static, str> { pub enum Helper { AsyncToGenerator, ObjectSpread2, + DefineProperty, } impl Helper { @@ -146,6 +147,7 @@ impl Helper { match self { Self::AsyncToGenerator => "asyncToGenerator", Self::ObjectSpread2 => "objectSpread2", + Self::DefineProperty => "defineProperty", } } } diff --git a/crates/oxc_transformer/src/es2022/class_properties.rs b/crates/oxc_transformer/src/es2022/class_properties.rs new file mode 100644 index 00000000000000..67b3e9da109c86 --- /dev/null +++ b/crates/oxc_transformer/src/es2022/class_properties.rs @@ -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: +//! * +//! * +//! * +//! * Class properties TC39 proposal: + +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 + } +} diff --git a/crates/oxc_transformer/src/es2022/mod.rs b/crates/oxc_transformer/src/es2022/mod.rs index d2feb9f35c7cdb..c0ad06a5fb0838 100644 --- a/crates/oxc_transformer/src/es2022/mod.rs +++ b/crates/oxc_transformer/src/es2022/mod.rs @@ -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>, } -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); + } } } diff --git a/crates/oxc_transformer/src/es2022/options.rs b/crates/oxc_transformer/src/es2022/options.rs index b3afdee6ce1931..7622a6b96dcc90 100644 --- a/crates/oxc_transformer/src/es2022/options.rs +++ b/crates/oxc_transformer/src/es2022/options.rs @@ -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, } impl ES2022Options { @@ -15,6 +18,11 @@ impl ES2022Options { self } + pub fn with_class_properties(&mut self, option: Option) -> &mut Self { + self.class_properties = option; + self + } + #[must_use] pub fn from_targets_and_bugfixes(targets: Option<&Versions>, bugfixes: bool) -> Self { Self { @@ -23,6 +31,8 @@ impl ES2022Options { targets, bugfixes, ), + class_properties: can_enable_plugin("transform-class-properties", targets, bugfixes) + .then(Default::default), } } } diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 1f945142a9759d..2e28cf5fc351bf 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -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), @@ -118,7 +118,7 @@ struct TransformerImpl<'a, 'ctx> { // NOTE: all callbacks must run in order. x0_typescript: Option>, 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, diff --git a/crates/oxc_transformer/src/options/transformer.rs b/crates/oxc_transformer/src/options/transformer.rs index 24972e25a52b85..61ee510deef092 100644 --- a/crates/oxc_transformer/src/options/transformer.rs +++ b/crates/oxc_transformer/src/options/transformer.rs @@ -15,7 +15,7 @@ use crate::{ es2019::ES2019Options, es2020::ES2020Options, es2021::ES2021Options, - es2022::ES2022Options, + es2022::{ClassPropertiesOptions, ES2022Options}, jsx::JsxOptions, options::babel::BabelOptions, regexp::RegExpOptions, @@ -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() diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index c1c834685e3733..e0b711af2c5e51 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,6 +1,6 @@ commit: d20b314c -Passed: 357/1058 +Passed: 362/1162 # All Passed: * babel-plugin-transform-class-static-block @@ -1439,6 +1439,305 @@ x Output mismatch x Output mismatch +# babel-plugin-transform-class-properties (4/103) +* assumption-constantSuper/complex-super-class/input.js +x Output mismatch + +* assumption-constantSuper/instance-field/input.js +x Output mismatch + +* assumption-constantSuper/static-field/input.js +x Output mismatch + +* assumption-noDocumentAll/optional-chain-before-member-call/input.js +x Output mismatch + +* assumption-noDocumentAll/optional-chain-cast-to-boolean/input.js +x Output mismatch + +* assumption-noUninitializedPrivateFieldAccess/static-private/input.js +x Output mismatch + +* assumption-setPublicClassFields/computed/input.js +x Output mismatch + +* assumption-setPublicClassFields/constructor-collision/input.js +x Output mismatch + +* assumption-setPublicClassFields/derived/input.js +x Output mismatch + +* assumption-setPublicClassFields/foobar/input.js +x Output mismatch + +* assumption-setPublicClassFields/instance/input.js +x Output mismatch + +* assumption-setPublicClassFields/instance-computed/input.js +x Output mismatch + +* assumption-setPublicClassFields/instance-undefined/input.js +x Output mismatch + +* assumption-setPublicClassFields/length-name-use-define/input.js +x Output mismatch + +* assumption-setPublicClassFields/non-block-arrow-func/input.mjs +x Output mismatch + +* assumption-setPublicClassFields/regression-T2983/input.mjs +x Output mismatch + +* assumption-setPublicClassFields/regression-T6719/input.js +x Output mismatch + +* assumption-setPublicClassFields/regression-T7364/input.mjs +x Output mismatch + +* assumption-setPublicClassFields/static/input.js +x Output mismatch + +* assumption-setPublicClassFields/static-class-binding/input.js +x Output mismatch + +* assumption-setPublicClassFields/static-export/input.mjs +x Output mismatch + +* assumption-setPublicClassFields/static-infer-name/input.js +x Output mismatch + +* assumption-setPublicClassFields/static-super/input.js +x Output mismatch + +* assumption-setPublicClassFields/static-super-loose/input.js +x Output mismatch + +* assumption-setPublicClassFields/static-this/input.js +x Output mismatch + +* assumption-setPublicClassFields/static-undefined/input.js +x Output mismatch + +* assumption-setPublicClassFields/super-call/input.js +x Output mismatch + +* assumption-setPublicClassFields/super-expression/input.js +x Output mismatch + +* assumption-setPublicClassFields/super-statement/input.js +x Output mismatch + +* assumption-setPublicClassFields/super-with-collision/input.js +x Output mismatch + +* class-name-tdz/general/input.js +x Output mismatch + +* class-name-tdz/static-loose/input.js +x Output mismatch + +* compile-to-class/constructor-collision-ignores-types/input.js +x Output mismatch + +* compile-to-class/constructor-collision-ignores-types-loose/input.js +x Output mismatch + +* compile-to-class/preserve-comments/input.js +x Output mismatch + +* private/class-shadow-builtins/input.mjs +x Output mismatch + +* private/logical-assignment/input.js +x Output mismatch + +* private/native-classes/input.js +x Output mismatch + +* private/non-block-arrow-func/input.mjs +x Output mismatch + +* private/optional-chain-before-member-call/input.js +x Output mismatch + +* private/optional-chain-before-property/input.js +x Output mismatch + +* private/optional-chain-cast-to-boolean/input.js +x Output mismatch + +* private/optional-chain-delete-property/input.js +x Output mismatch + +* private/optional-chain-in-function-param/input.js +x Output mismatch + +* private/optional-chain-member-optional-call/input.js +x Output mismatch + +* private/optional-chain-member-optional-call-spread-arguments/input.js +x Output mismatch + +* private/optional-chain-optional-member-call/input.js +x Output mismatch + +* private/optional-chain-optional-property/input.js +x Output mismatch + +* private/parenthesized-optional-member-call/input.js +x Output mismatch + +* private/reevaluated/input.js +x Output mismatch + +* private/regression-T2983/input.mjs +x Output mismatch + +* private/regression-T6719/input.js +x Output mismatch + +* private/regression-T7364/input.mjs +x Output mismatch + +* private/static/input.js +x Output mismatch + +* private/static-export/input.mjs +x Output mismatch + +* private/static-infer-name/input.js +x Output mismatch + +* private/static-inherited/input.js +x Output mismatch + +* private/static-self-field/input.js +x Output mismatch + +* private/static-shadow/input.js +x Output mismatch + +* private/static-undefined/input.js +x Output mismatch + +* private-loose/class-shadow-builtins/input.mjs +x Output mismatch + +* private-loose/logical-assignment/input.js +x Output mismatch + +* private-loose/native-classes/input.js +x Output mismatch + +* private-loose/non-block-arrow-func/input.mjs +x Output mismatch + +* private-loose/optional-chain-before-member-call/input.js +x Output mismatch + +* private-loose/optional-chain-before-property/input.js +x Output mismatch + +* private-loose/optional-chain-cast-to-boolean/input.js +x Output mismatch + +* private-loose/optional-chain-delete-property/input.js +x Output mismatch + +* private-loose/optional-chain-in-function-param/input.js +x Output mismatch + +* private-loose/optional-chain-member-optional-call/input.js +x Output mismatch + +* private-loose/optional-chain-member-optional-call-spread-arguments/input.js +x Output mismatch + +* private-loose/optional-chain-optional-member-call/input.js +x Output mismatch + +* private-loose/optional-chain-optional-property/input.js +x Output mismatch + +* private-loose/parenthesized-optional-member-call/input.js +x Output mismatch + +* private-loose/reevaluated/input.js +x Output mismatch + +* private-loose/static/input.js +x Output mismatch + +* private-loose/static-export/input.mjs +x Output mismatch + +* private-loose/static-infer-name/input.js +x Output mismatch + +* private-loose/static-inherited/input.js +x Output mismatch + +* private-loose/static-shadow/input.js +x Output mismatch + +* private-loose/static-undefined/input.js +x Output mismatch + +* public/class-shadow-builtins/input.mjs +x Output mismatch + +* public/derived-super-in-default-params/input.js +x Output mismatch + +* public/derived-super-in-default-params-complex/input.js +x Output mismatch + +* public/derived-super-in-default-params-in-arrow/input.js +x Output mismatch + +* public/foobar/input.js +x Output mismatch + +* public/native-classes/input.js +x Output mismatch + +* public/regression-T7364/input.mjs +x Output mismatch + +* public-loose/class-shadow-builtins/input.mjs +x Output mismatch + +* public-loose/regression-T7364/input.mjs +x Output mismatch + +* regression/15098/input.js +x Output mismatch + +* regression/6153/input.js +x Output mismatch + +* regression/6154/input.js +x Output mismatch + +* regression/7371/input.js +x Output mismatch + +* regression/7951/input.mjs +x Output mismatch + +* regression/8110/input.js +x Output mismatch + +* regression/8882/input.js +x Output mismatch + +* regression/T7364/input.mjs +x Output mismatch + +* regression/multiple-super-in-termary/input.js +x Output mismatch + + # babel-plugin-transform-nullish-coalescing-operator (5/12) * assumption-noDocumentAll/transform/input.js x Output mismatch @@ -1732,7 +2031,7 @@ rebuilt : ScopeId(1): [] x Output mismatch -# babel-plugin-transform-typescript (39/152) +# babel-plugin-transform-typescript (40/153) * cast/as-expression/input.ts Unresolved references mismatch: after transform: ["T", "x"] diff --git a/tasks/transform_conformance/snapshots/babel_exec.snap.md b/tasks/transform_conformance/snapshots/babel_exec.snap.md index 05fa232b701193..28fb6fff1b6284 100644 --- a/tasks/transform_conformance/snapshots/babel_exec.snap.md +++ b/tasks/transform_conformance/snapshots/babel_exec.snap.md @@ -1,6 +1,6 @@ commit: d20b314c -Passed: 45/73 +Passed: 90/120 # All Passed: * babel-plugin-transform-class-static-block @@ -24,6 +24,14 @@ exec failed exec failed +# babel-plugin-transform-class-properties (45/47) +* private/parenthesized-optional-member-call/exec.js +exec failed + +* private-loose/parenthesized-optional-member-call/exec.js +exec failed + + # babel-plugin-transform-object-rest-spread (15/31) * assumption-objectRestNoSymbols/rest-ignore-symbols/exec.js exec failed diff --git a/tasks/transform_conformance/src/constants.rs b/tasks/transform_conformance/src/constants.rs index 3455c0160e922c..7dc38fff3ae6cf 100644 --- a/tasks/transform_conformance/src/constants.rs +++ b/tasks/transform_conformance/src/constants.rs @@ -3,7 +3,7 @@ pub(crate) const PLUGINS: &[&str] = &[ // // ES2024 // "babel-plugin-transform-unicode-sets-regex", // // ES2022 - // "babel-plugin-transform-class-properties", + "babel-plugin-transform-class-properties", "babel-plugin-transform-class-static-block", // "babel-plugin-transform-private-methods", // "babel-plugin-transform-private-property-in-object", @@ -63,7 +63,6 @@ pub(crate) const PLUGINS: &[&str] = &[ pub(crate) const PLUGINS_NOT_SUPPORTED_YET: &[&str] = &[ "proposal-decorators", - "transform-class-properties", "transform-classes", "transform-destructuring", "transform-modules-commonjs",