diff --git a/crates/oxc_ast/src/ast_builder.rs b/crates/oxc_ast/src/ast_builder.rs index e950184d51ed0..37e6df59eba29 100644 --- a/crates/oxc_ast/src/ast_builder.rs +++ b/crates/oxc_ast/src/ast_builder.rs @@ -66,6 +66,11 @@ 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 program( &self, span: Span, @@ -715,6 +720,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..79d1e5b171206 --- /dev/null +++ b/crates/oxc_transformer/src/es2022/class_static_block.rs @@ -0,0 +1,105 @@ +use oxc_ast::{ast::*, AstBuilder}; +use oxc_span::{Atom, Span}; + +use std::{collections::HashSet, mem, ops::DerefMut, 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(|p| match p { + PropertyKey::PrivateIdentifier(p) => Some(p.name.clone()), + _ => None, + }) + .collect(); + + let mut i = 0; + for element in class_body.body.iter_mut() { + let ClassElement::StaticBlock(block) = element else { + continue; + }; + + 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 = (block.body.len() == 1 + && matches!(block.body[0], Statement::ExpressionStatement(..))) + .then(|| { + // We special-case the single expression case to avoid the iife, since it's common. + let stmt = self.ast.move_statement(&mut block.body.deref_mut()[0]); + match stmt { + Statement::ExpressionStatement(mut expr_stmt) => { + self.ast.move_expression(&mut expr_stmt.expression) + } + _ => unreachable!(), + } + }) + .unwrap_or_else(|| { + let statements = mem::replace(&mut block.body, self.ast.new_vec()); + let callee = self.ast.parenthesized_expression( + Span::default(), + self.ast.arrow_expression( + Span::default(), + false, + false, + false, + self.ast.formal_parameters( + Span::default(), + FormalParameterKind::ArrowFormalParameters, + self.ast.new_vec(), + None, + ), + self.ast.function_body(Span::default(), self.ast.new_vec(), statements), + None, + None, + ), + ); + + self.ast.call_expression(Span::default(), callee, self.ast.new_vec(), false, None) + }); + + *element = self.ast.class_property( + block.span, + key, + Some(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