feat(transformer): Class Static Block (#962)

This commit is contained in:
magic-akari 2023-10-07 19:16:40 -05:00 committed by GitHub
parent 77dd0f1395
commit 9ad2634091
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 189 additions and 5 deletions

View file

@ -414,6 +414,13 @@ impl<'a> PropertyKey<'a> {
matches!(self, Self::PrivateIdentifier(_))
}
pub fn private_name(&self) -> Option<Atom> {
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,

View file

@ -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<Expression<'a>>,
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,

View file

@ -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:
/// * <https://babel.dev/docs/babel-plugin-transform-class-static-block>
/// * <https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-class-static-block>
pub struct ClassStaticBlock<'a> {
ast: Rc<AstBuilder<'a>>,
}
impl<'a> ClassStaticBlock<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> 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<Atom> = 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<Atom>, 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
}

View file

@ -0,0 +1,3 @@
mod class_static_block;
pub use class_static_block::ClassStaticBlock;

View file

@ -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<TypeScript<'a>>,
react_jsx: Option<ReactJsx<'a>>,
// es2022
es2022_class_static_block: Option<es2022::ClassStaticBlock<'a>>,
// es2021
es2021_logical_assignment_operators: Option<LogicalAssignmentOperators<'a>>,
// 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);
});
}
}

View file

@ -12,6 +12,7 @@ pub enum TransformTarget {
ES2016,
ES2019,
ES2021,
ES2022,
#[default]
ESNext,
}

View file

@ -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