diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 14bbe1adf5ded..d279330d9a215 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -414,6 +414,13 @@ impl<'a> PropertyKey<'a> { matches!(self, Self::PrivateIdentifier(_)) } + pub fn private_name(&self) -> Option { + match self { + Self::PrivateIdentifier(ident) => Some(ident.name.clone()), + _ => None, + } + } + pub fn is_specific_id(&self, name: &str) -> bool { match self { PropertyKey::Identifier(ident) => ident.name == name, diff --git a/crates/oxc_ast/src/ast_builder.rs b/crates/oxc_ast/src/ast_builder.rs index e950184d51ed0..6ecadd297fa6f 100644 --- a/crates/oxc_ast/src/ast_builder.rs +++ b/crates/oxc_ast/src/ast_builder.rs @@ -66,6 +66,15 @@ impl<'a> AstBuilder<'a> { mem::replace(expr, null_expr) } + pub fn move_statement(&self, stmt: &mut Statement<'a>) -> Statement<'a> { + let empty_stmt = self.empty_statement(stmt.span()); + mem::replace(stmt, empty_stmt) + } + + pub fn move_statement_vec(&self, stmts: &mut Vec<'a, Statement<'a>>) -> Vec<'a, Statement<'a>> { + mem::replace(stmts, self.new_vec()) + } + pub fn program( &self, span: Span, @@ -715,6 +724,32 @@ impl<'a> AstBuilder<'a> { ClassElement::StaticBlock(self.alloc(StaticBlock { span, body })) } + pub fn class_property( + &self, + span: Span, + key: PropertyKey<'a>, + value: Option>, + computed: bool, + r#static: bool, + decorators: Vec<'a, Decorator<'a>>, + ) -> ClassElement<'a> { + ClassElement::PropertyDefinition(self.alloc(PropertyDefinition { + span, + key, + value, + computed, + r#static, + declare: false, + r#override: false, + optional: false, + definite: false, + readonly: false, + type_annotation: None, + accessibility: None, + decorators, + })) + } + pub fn accessor_property( &self, span: Span, diff --git a/crates/oxc_transformer/src/es2022/class_static_block.rs b/crates/oxc_transformer/src/es2022/class_static_block.rs new file mode 100644 index 0000000000000..461e35fc23014 --- /dev/null +++ b/crates/oxc_transformer/src/es2022/class_static_block.rs @@ -0,0 +1,124 @@ +use oxc_ast::{ast::*, AstBuilder}; +use oxc_span::{Atom, Span}; + +use std::{collections::HashSet, rc::Rc}; + +/// ES2022: Class Static Block +/// +/// References: +/// * +/// * +pub struct ClassStaticBlock<'a> { + ast: Rc>, +} + +impl<'a> ClassStaticBlock<'a> { + pub fn new(ast: Rc>) -> Self { + Self { ast } + } + + pub fn transform_class_body<'b>(&mut self, class_body: &'b mut ClassBody<'a>) { + if !class_body.body.iter().any(|e| matches!(e, ClassElement::StaticBlock(..))) { + return; + } + + let private_names: HashSet = class_body + .body + .iter() + .filter_map(ClassElement::property_key) + .filter_map(PropertyKey::private_name) + .collect(); + + let mut i = 0; + for element in class_body.body.iter_mut() { + let ClassElement::StaticBlock(block) = element else { + continue; + }; + + let span = block.span; + + let static_block_private_id = generate_uid(&private_names, &mut i); + let key = PropertyKey::PrivateIdentifier(self.ast.alloc(PrivateIdentifier { + span: Span::default(), + name: static_block_private_id.clone(), + })); + + let value = match block.body.len() { + 0 => None, + 1 if matches!(block.body[0], Statement::ExpressionStatement(..)) => { + // We special-case the single expression case to avoid the iife, since it's common. + // + // We prefer to emit: + // ```JavaScript + // class Foo { + // static bar = 42; + // static #_ = this.foo = Foo.bar; + // } + // ``` + // instead of: + // ```JavaScript + // class Foo { + // static bar = 42; + // static #_ = (() => this.foo = Foo.bar)(); + // } + // ``` + + let stmt = self.ast.move_statement(&mut (*block.body)[0]); + let Statement::ExpressionStatement(mut expr_stmt) = stmt else { + unreachable!() + }; + let value = self.ast.move_expression(&mut expr_stmt.expression); + Some(value) + } + _ => { + let params = self.ast.formal_parameters( + Span::default(), + FormalParameterKind::ArrowFormalParameters, + self.ast.new_vec(), + None, + ); + + let statements = self.ast.move_statement_vec(&mut block.body); + let function_body = + self.ast.function_body(Span::default(), self.ast.new_vec(), statements); + + let callee = self.ast.arrow_expression( + Span::default(), + false, + false, + false, + params, + function_body, + None, + None, + ); + + let callee = self.ast.parenthesized_expression(Span::default(), callee); + + let value = self.ast.call_expression( + Span::default(), + callee, + self.ast.new_vec(), + false, + None, + ); + Some(value) + } + }; + + *element = self.ast.class_property(span, key, value, false, true, self.ast.new_vec()); + } + } +} + +fn generate_uid(deny_list: &HashSet, i: &mut u32) -> Atom { + *i += 1; + + let mut uid: Atom = if *i == 1 { "_".to_string() } else { format!("_{i}") }.into(); + while deny_list.contains(&uid) { + *i += 1; + uid = format!("_{i}").into(); + } + + uid +} diff --git a/crates/oxc_transformer/src/es2022/mod.rs b/crates/oxc_transformer/src/es2022/mod.rs new file mode 100644 index 0000000000000..ea8bfb6fffd72 --- /dev/null +++ b/crates/oxc_transformer/src/es2022/mod.rs @@ -0,0 +1,3 @@ +mod class_static_block; + +pub use class_static_block::ClassStaticBlock; diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index ca941a6d03be9..2a72345ee6bcb 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -11,6 +11,7 @@ mod es2015; mod es2016; mod es2019; mod es2021; +mod es2022; mod options; mod react_jsx; mod typescript; @@ -35,6 +36,8 @@ pub use crate::options::{ pub struct Transformer<'a> { typescript: Option>, react_jsx: Option>, + // es2022 + es2022_class_static_block: Option>, // es2021 es2021_logical_assignment_operators: Option>, // es2019 @@ -60,6 +63,9 @@ impl<'a> Transformer<'a> { if let Some(react_options) = options.react { t.react_jsx.replace(ReactJsx::new(Rc::clone(&ast), react_options)); } + if options.target < TransformTarget::ES2022 { + t.es2022_class_static_block.replace(es2022::ClassStaticBlock::new(Rc::clone(&ast))); + } if options.target < TransformTarget::ES2021 { t.es2021_logical_assignment_operators .replace(LogicalAssignmentOperators::new(Rc::clone(&ast))); @@ -109,4 +115,12 @@ impl<'a, 'b> VisitMut<'a, 'b> for Transformer<'a> { self.visit_expression(init); } } + + fn visit_class_body(&mut self, class_body: &'b mut ClassBody<'a>) { + self.es2022_class_static_block.as_mut().map(|t| t.transform_class_body(class_body)); + + class_body.body.iter_mut().for_each(|class_element| { + self.visit_class_element(class_element); + }); + } } diff --git a/crates/oxc_transformer/src/options.rs b/crates/oxc_transformer/src/options.rs index c633716f6b165..01aa5ee78a6eb 100644 --- a/crates/oxc_transformer/src/options.rs +++ b/crates/oxc_transformer/src/options.rs @@ -12,6 +12,7 @@ pub enum TransformTarget { ES2016, ES2019, ES2021, + ES2022, #[default] ESNext, } diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index 1a879b2590a6f..0b924f6b39578 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 91/1078 +Passed: 95/1078 # babel-plugin-transform-class-properties * Failed: assumption-constantSuper/complex-super-class/input.js @@ -269,10 +269,6 @@ Passed: 91/1078 * Passed: public-loose/arrow-this-without-transform/input.js # babel-plugin-transform-class-static-block -* Failed: class-static-block/class-binding/input.js -* Failed: class-static-block/class-declaration/input.js -* Failed: class-static-block/in-class-heritage/input.js -* Failed: class-static-block/multiple-static-initializers/input.js * Failed: class-static-block/name-conflict/input.js * Failed: class-static-block/new-target/input.js * Failed: class-static-block/preserve-comments/input.js @@ -290,6 +286,10 @@ Passed: 91/1078 * Failed: integration-loose/name-conflict/input.js * Failed: integration-loose/preserve-comments/input.js * Failed: integration-loose/super-static-block/input.js +* Passed: class-static-block/class-binding/input.js +* Passed: class-static-block/class-declaration/input.js +* Passed: class-static-block/in-class-heritage/input.js +* Passed: class-static-block/multiple-static-initializers/input.js * Passed: integration-loose/.new-target/input.js # babel-plugin-transform-private-methods