diff --git a/Cargo.lock b/Cargo.lock index d4779dc68f61f..3815054ba024c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2018,6 +2018,7 @@ dependencies = [ "cow-utils", "dashmap 6.1.0", "indexmap", + "itoa", "oxc-browserslist", "oxc_allocator", "oxc_ast", diff --git a/crates/oxc_semantic/src/scope.rs b/crates/oxc_semantic/src/scope.rs index cb433e85b6dd7..91b0d599224f9 100644 --- a/crates/oxc_semantic/src/scope.rs +++ b/crates/oxc_semantic/src/scope.rs @@ -173,6 +173,17 @@ impl ScopeTree { } } + /// Delete a scope. + pub fn delete_scope(&mut self, scope_id: ScopeId) { + if self.build_child_ids { + self.child_ids[scope_id].clear(); + let parent_id = self.parent_ids[scope_id]; + if let Some(parent_id) = parent_id { + self.child_ids[parent_id].retain(|&child_id| child_id != scope_id); + } + } + } + /// Get a variable binding by name that was declared in the top-level scope #[inline] pub fn get_root_binding(&self, name: &str) -> Option { diff --git a/crates/oxc_transformer/Cargo.toml b/crates/oxc_transformer/Cargo.toml index 492b79a92c613..b33d4560813bd 100644 --- a/crates/oxc_transformer/Cargo.toml +++ b/crates/oxc_transformer/Cargo.toml @@ -38,6 +38,7 @@ base64 = { workspace = true } cow-utils = { workspace = true } dashmap = { workspace = true } indexmap = { workspace = true } +itoa = { workspace = true } ropey = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } 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..8f637a61f71df --- /dev/null +++ b/crates/oxc_transformer/src/es2022/class_static_block.rs @@ -0,0 +1,446 @@ +//! ES2022: Class Static Block +//! +//! This plugin transforms class static blocks (`class C { static { foo } }`) to an equivalent +//! using private fields (`class C { static #_ = foo }`). +//! +//! > This plugin is included in `preset-env`, in ES2022 +//! +//! ## Example +//! +//! Input: +//! ```js +//! class C { +//! static { +//! foo(); +//! } +//! static { +//! foo(); +//! bar(); +//! } +//! } +//! ``` +//! +//! Output: +//! ```js +//! class C { +//! static #_ = foo(); +//! static #_2 = (() => { +//! foo(); +//! bar(); +//! })(); +//! } +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-class-static-block](https://babel.dev/docs/babel-plugin-transform-class-static-block). +//! +//! ## References: +//! * Babel plugin implementation: +//! * Class static initialization blocks TC39 proposal: + +use itoa::Buffer as ItoaBuffer; + +use oxc_allocator::String as AString; +use oxc_ast::{ast::*, Visit, NONE}; +use oxc_semantic::SymbolTable; +use oxc_span::SPAN; +use oxc_syntax::{ + reference::ReferenceFlags, + scope::{ScopeFlags, ScopeId}, +}; +use oxc_traverse::{Traverse, TraverseCtx}; + +pub struct ClassStaticBlock; + +impl ClassStaticBlock { + pub fn new() -> Self { + Self + } +} + +impl<'a> Traverse<'a> for ClassStaticBlock { + fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { + // Loop through class body elements and: + // 1. Find if there are any `StaticBlock`s. + // 2. Collate list of private keys matching `#_` or `#_[1-9]...`. + // + // Don't collate private keys list conditionally only if a static block is found, as usually + // there will be no matching private keys, so those checks are cheap and will not allocate. + let mut has_static_block = false; + let mut keys = Keys::default(); + for element in &body.body { + let key = match element { + ClassElement::StaticBlock(_) => { + has_static_block = true; + continue; + } + ClassElement::MethodDefinition(def) => &def.key, + ClassElement::PropertyDefinition(def) => &def.key, + ClassElement::AccessorProperty(def) => &def.key, + ClassElement::TSIndexSignature(_) => continue, + }; + + if let PropertyKey::PrivateIdentifier(id) = key { + keys.reserve(id.name.as_str()); + } + } + + // Transform static blocks + if !has_static_block { + return; + } + + for element in body.body.iter_mut() { + if let ClassElement::StaticBlock(block) = element { + *element = Self::convert_block_to_private_field(block, &mut keys, ctx); + } + } + } +} + +impl ClassStaticBlock { + /// Convert static block to private field. + /// `static { foo }` -> `static #_ = foo;` + /// `static { foo; bar; }` -> `static #_ = (() => { foo; bar; })();` + fn convert_block_to_private_field<'a>( + block: &mut StaticBlock<'a>, + keys: &mut Keys<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ClassElement<'a> { + let expr = Self::convert_block_to_expression(block, ctx); + + let key = keys.get_unique(ctx); + let key = ctx.ast.property_key_private_identifier(SPAN, key); + + ctx.ast.class_element_property_definition( + PropertyDefinitionType::PropertyDefinition, + block.span, + ctx.ast.vec(), + key, + Some(expr), + false, + true, + false, + false, + false, + false, + false, + NONE, + None, + ) + } + + /// Convert static block to expression which will be value of private field. + /// `static { foo }` -> `foo` + /// `static { foo; bar; }` -> `(() => { foo; bar; })()` + fn convert_block_to_expression<'a>( + block: &mut StaticBlock<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let scope_id = block.scope_id.get().unwrap(); + + // If block contains only a single `ExpressionStatement`, no need to wrap in an IIFE. + // `static { foo }` -> `foo` + // TODO(improve-on-babel): If block has no statements, could remove it entirely. + let stmts = &mut block.body; + if stmts.len() == 1 { + if let Statement::ExpressionStatement(stmt) = stmts.first_mut().unwrap() { + return Self::convert_block_with_single_expression_to_expression( + &mut stmt.expression, + scope_id, + ctx, + ); + } + } + + // Convert block to arrow function IIFE. + // `static { foo; bar; }` -> `(() => { foo; bar; })()` + + // Re-use the static block's scope for the arrow function. + // Always strict mode since we're in a class. + *ctx.scopes_mut().get_flags_mut(scope_id) = + ScopeFlags::Function | ScopeFlags::Arrow | ScopeFlags::StrictMode; + + let stmts = ctx.ast.move_vec(stmts); + let params = ctx.ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::ArrowFormalParameters, + ctx.ast.vec(), + NONE, + ); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), stmts); + let arrow = Expression::ArrowFunctionExpression( + ctx.ast.alloc_arrow_function_expression_with_scope_id( + SPAN, false, false, NONE, params, NONE, body, scope_id, + ), + ); + ctx.ast.expression_call(SPAN, arrow, NONE, ctx.ast.vec(), false) + } + + /// Convert static block to expression which will be value of private field, + /// where the static block contains only a single expression. + /// `static { foo }` -> `foo` + fn convert_block_with_single_expression_to_expression<'a>( + expr: &mut Expression<'a>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let expr = ctx.ast.move_expression(expr); + + // Remove the scope for the static block from the scope chain + ctx.remove_scope_for_expression(scope_id, &expr); + + // If expression is an assignment, left side has moved from a write-only position to a read + write one. + // `static { x = 1; }` -> `static #_ = x = 1;` + // So set `ReferenceFlags::Read` on the left side. + if let Expression::AssignmentExpression(assign_expr) = &expr { + if assign_expr.operator == AssignmentOperator::Assign { + let mut setter = ReferenceFlagsSetter { symbols: ctx.symbols_mut() }; + setter.visit_assignment_target(&assign_expr.left); + } + } + + expr + } +} + +/// Visitor which sets `ReferenceFlags::Read` flag on all `IdentifierReference`s. +/// It skips `MemberExpression`s, because their flags are not affected by the change in position. +struct ReferenceFlagsSetter<'s> { + symbols: &'s mut SymbolTable, +} + +impl<'a, 's> Visit<'a> for ReferenceFlagsSetter<'s> { + fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) { + let reference_id = ident.reference_id().unwrap(); + let reference = self.symbols.get_reference_mut(reference_id); + *reference.flags_mut() |= ReferenceFlags::Read; + } + + fn visit_member_expression(&mut self, _member_expr: &MemberExpression<'a>) { + // Don't traverse further + } +} + +/// Store of private identifier keys matching `#_` or `#_[1-9]...`. +/// +/// Most commonly there will be no existing keys matching this pattern +/// (why would you prefix a private key with `_`?). +/// It's also uncommon to have more than 1 static block in a class. +/// +/// Therefore common case is only 1 static block, which will use key `#_`. +/// So store whether `#_` is in set as a separate `bool`, to make a fast path this common case, +/// which does not involve any allocations (`numbered` will remain empty). +/// +/// Use a `Vec` rather than a `HashMap`, because number of matching private keys is usually small, +/// and `Vec` is lower overhead in that case. +#[derive(Default)] +struct Keys<'a> { + /// `true` if keys includes `#_`. + underscore: bool, + /// Keys matching `#_[1-9]...`. Stored without the `_` prefix. + numbered: Vec<&'a str>, +} + +impl<'a> Keys<'a> { + /// Add a key to set. + /// + /// Key will only be added to set if it's `_`, or starts with `_[1-9]`. + fn reserve(&mut self, key: &'a str) { + let mut bytes = key.as_bytes().iter().copied(); + if bytes.next() != Some(b'_') { + return; + } + + match bytes.next() { + None => { + self.underscore = true; + } + Some(b'1'..=b'9') => { + self.numbered.push(&key[1..]); + } + _ => {} + } + } + + /// Get a key which is not in the set. + /// + /// Returned key will be either `_`, or `_` starting with `_2`. + #[inline] + fn get_unique(&mut self, ctx: &mut TraverseCtx<'a>) -> Atom<'a> { + #[expect(clippy::if_not_else)] + if !self.underscore { + self.underscore = true; + Atom::from("_") + } else { + self.get_unique_slow(ctx) + } + } + + // `#[cold]` and `#[inline(never)]` as it should be very rare to need a key other than `#_`. + #[cold] + #[inline(never)] + fn get_unique_slow(&mut self, ctx: &mut TraverseCtx<'a>) -> Atom<'a> { + // Source text length is limited to `u32::MAX` so impossible to have more than `u32::MAX` + // private keys. So `u32` is sufficient here. + let mut i = 2u32; + let mut buffer = ItoaBuffer::new(); + let mut num_str; + loop { + num_str = buffer.format(i); + if !self.numbered.contains(&num_str) { + break; + } + i += 1; + } + + let mut key = AString::with_capacity_in(num_str.len() + 1, ctx.ast.allocator); + key.push('_'); + key.push_str(num_str); + let key = Atom::from(key.into_bump_str()); + + self.numbered.push(&key.as_str()[1..]); + + key + } +} + +#[cfg(test)] +mod test { + use oxc_allocator::Allocator; + use oxc_semantic::{ScopeTree, SymbolTable}; + use oxc_traverse::TraverseCtx; + + use super::Keys; + + macro_rules! setup { + ($ctx:ident) => { + let allocator = Allocator::default(); + let scopes = ScopeTree::default(); + let symbols = SymbolTable::default(); + let mut $ctx = TraverseCtx::new(scopes, symbols, &allocator); + }; + } + + #[test] + fn keys_no_reserved() { + setup!(ctx); + + let mut keys = Keys::default(); + + assert_eq!(keys.get_unique(&mut ctx), "_"); + assert_eq!(keys.get_unique(&mut ctx), "_2"); + assert_eq!(keys.get_unique(&mut ctx), "_3"); + assert_eq!(keys.get_unique(&mut ctx), "_4"); + assert_eq!(keys.get_unique(&mut ctx), "_5"); + assert_eq!(keys.get_unique(&mut ctx), "_6"); + assert_eq!(keys.get_unique(&mut ctx), "_7"); + assert_eq!(keys.get_unique(&mut ctx), "_8"); + assert_eq!(keys.get_unique(&mut ctx), "_9"); + assert_eq!(keys.get_unique(&mut ctx), "_10"); + assert_eq!(keys.get_unique(&mut ctx), "_11"); + assert_eq!(keys.get_unique(&mut ctx), "_12"); + } + + #[test] + fn keys_no_relevant_reserved() { + setup!(ctx); + + let mut keys = Keys::default(); + keys.reserve("a"); + keys.reserve("foo"); + keys.reserve("__"); + keys.reserve("_0"); + keys.reserve("_1"); + keys.reserve("_a"); + keys.reserve("_foo"); + keys.reserve("_2foo"); + + assert_eq!(keys.get_unique(&mut ctx), "_"); + assert_eq!(keys.get_unique(&mut ctx), "_2"); + assert_eq!(keys.get_unique(&mut ctx), "_3"); + } + + #[test] + fn keys_reserved_underscore() { + setup!(ctx); + + let mut keys = Keys::default(); + keys.reserve("_"); + + assert_eq!(keys.get_unique(&mut ctx), "_2"); + assert_eq!(keys.get_unique(&mut ctx), "_3"); + assert_eq!(keys.get_unique(&mut ctx), "_4"); + } + + #[test] + fn keys_reserved_numbers() { + setup!(ctx); + + let mut keys = Keys::default(); + keys.reserve("_2"); + keys.reserve("_4"); + keys.reserve("_11"); + + assert_eq!(keys.get_unique(&mut ctx), "_"); + assert_eq!(keys.get_unique(&mut ctx), "_3"); + assert_eq!(keys.get_unique(&mut ctx), "_5"); + assert_eq!(keys.get_unique(&mut ctx), "_6"); + assert_eq!(keys.get_unique(&mut ctx), "_7"); + assert_eq!(keys.get_unique(&mut ctx), "_8"); + assert_eq!(keys.get_unique(&mut ctx), "_9"); + assert_eq!(keys.get_unique(&mut ctx), "_10"); + assert_eq!(keys.get_unique(&mut ctx), "_12"); + } + + #[test] + fn keys_reserved_later_numbers() { + setup!(ctx); + + let mut keys = Keys::default(); + keys.reserve("_5"); + keys.reserve("_4"); + keys.reserve("_12"); + keys.reserve("_13"); + + assert_eq!(keys.get_unique(&mut ctx), "_"); + assert_eq!(keys.get_unique(&mut ctx), "_2"); + assert_eq!(keys.get_unique(&mut ctx), "_3"); + assert_eq!(keys.get_unique(&mut ctx), "_6"); + assert_eq!(keys.get_unique(&mut ctx), "_7"); + assert_eq!(keys.get_unique(&mut ctx), "_8"); + assert_eq!(keys.get_unique(&mut ctx), "_9"); + assert_eq!(keys.get_unique(&mut ctx), "_10"); + assert_eq!(keys.get_unique(&mut ctx), "_11"); + assert_eq!(keys.get_unique(&mut ctx), "_14"); + } + + #[test] + fn keys_reserved_underscore_and_numbers() { + setup!(ctx); + + let mut keys = Keys::default(); + keys.reserve("_2"); + keys.reserve("_4"); + keys.reserve("_"); + + assert_eq!(keys.get_unique(&mut ctx), "_3"); + assert_eq!(keys.get_unique(&mut ctx), "_5"); + assert_eq!(keys.get_unique(&mut ctx), "_6"); + } + + #[test] + fn keys_reserved_underscore_and_later_numbers() { + setup!(ctx); + + let mut keys = Keys::default(); + keys.reserve("_5"); + keys.reserve("_4"); + keys.reserve("_"); + + assert_eq!(keys.get_unique(&mut ctx), "_2"); + assert_eq!(keys.get_unique(&mut ctx), "_3"); + assert_eq!(keys.get_unique(&mut ctx), "_6"); + } +} diff --git a/crates/oxc_transformer/src/es2022/mod.rs b/crates/oxc_transformer/src/es2022/mod.rs new file mode 100644 index 0000000000000..d2feb9f35c7cd --- /dev/null +++ b/crates/oxc_transformer/src/es2022/mod.rs @@ -0,0 +1,29 @@ +use oxc_ast::ast::*; +use oxc_traverse::{Traverse, TraverseCtx}; + +mod class_static_block; +mod options; + +use class_static_block::ClassStaticBlock; + +pub use options::ES2022Options; + +pub struct ES2022 { + options: ES2022Options, + // Plugins + class_static_block: ClassStaticBlock, +} + +impl ES2022 { + pub fn new(options: ES2022Options) -> Self { + Self { options, class_static_block: ClassStaticBlock::new() } + } +} + +impl<'a> Traverse<'a> for ES2022 { + 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); + } + } +} diff --git a/crates/oxc_transformer/src/es2022/options.rs b/crates/oxc_transformer/src/es2022/options.rs new file mode 100644 index 0000000000000..b3afdee6ce193 --- /dev/null +++ b/crates/oxc_transformer/src/es2022/options.rs @@ -0,0 +1,28 @@ +use serde::Deserialize; + +use crate::env::{can_enable_plugin, Versions}; + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2022Options { + #[serde(skip)] + pub class_static_block: bool, +} + +impl ES2022Options { + pub fn with_class_static_block(&mut self, enable: bool) -> &mut Self { + self.class_static_block = enable; + self + } + + #[must_use] + pub fn from_targets_and_bugfixes(targets: Option<&Versions>, bugfixes: bool) -> Self { + Self { + class_static_block: can_enable_plugin( + "transform-class-static-block", + targets, + bugfixes, + ), + } + } +} diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index a21cc920cc550..2a60003e24a73 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -30,6 +30,7 @@ mod es2018; mod es2019; mod es2020; mod es2021; +mod es2022; mod react; mod regexp; mod typescript; @@ -45,6 +46,7 @@ use es2018::ES2018; use es2019::ES2019; use es2020::ES2020; use es2021::ES2021; +use es2022::ES2022; use react::React; use regexp::RegExp; use typescript::TypeScript; @@ -93,6 +95,7 @@ impl<'a> Transformer<'a> { let mut transformer = TransformerImpl { x0_typescript: TypeScript::new(&self.options.typescript, &self.ctx), x1_react: React::new(self.options.react, ast_builder, &self.ctx), + x2_es2022: ES2022::new(self.options.es2022), 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), @@ -113,6 +116,7 @@ struct TransformerImpl<'a, 'ctx> { // NOTE: all callbacks must run in order. x0_typescript: TypeScript<'a, 'ctx>, x1_react: React<'a, 'ctx>, + x2_es2022: ES2022, x2_es2021: ES2021<'a, 'ctx>, x2_es2020: ES2020<'a, 'ctx>, x2_es2019: ES2019, @@ -170,6 +174,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.enter_class_body(body, ctx); + self.x2_es2022.enter_class_body(body, ctx); } fn enter_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { diff --git a/crates/oxc_transformer/src/options/transformer.rs b/crates/oxc_transformer/src/options/transformer.rs index 80cb4aa88c4df..3f52cae360da9 100644 --- a/crates/oxc_transformer/src/options/transformer.rs +++ b/crates/oxc_transformer/src/options/transformer.rs @@ -15,6 +15,7 @@ use crate::{ es2019::ES2019Options, es2020::ES2020Options, es2021::ES2021Options, + es2022::ES2022Options, options::babel::BabelOptions, react::JsxOptions, regexp::RegExpOptions, @@ -59,6 +60,8 @@ pub struct TransformOptions { pub es2021: ES2021Options, + pub es2022: ES2022Options, + pub helper_loader: HelperLoaderOptions, } @@ -97,6 +100,7 @@ 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 }, helper_loader: HelperLoaderOptions { mode: HelperLoaderMode::Runtime, ..Default::default() @@ -113,6 +117,7 @@ impl TransformOptions { es2019: ES2019Options::from_targets_and_bugfixes(targets, bugfixes), es2020: ES2020Options::from_targets_and_bugfixes(targets, bugfixes), es2021: ES2021Options::from_targets_and_bugfixes(targets, bugfixes), + es2022: ES2022Options::from_targets_and_bugfixes(targets, bugfixes), regexp: RegExpOptions::from_targets_and_bugfixes(targets, bugfixes), ..Default::default() } @@ -254,6 +259,11 @@ impl TransformOptions { get_enabled_plugin_options(plugin_name, options, targets.as_ref(), bugfixes).is_some() }); + transformer_options.es2022.with_class_static_block({ + let plugin_name = "transform-class-static-block"; + get_enabled_plugin_options(plugin_name, options, targets.as_ref(), bugfixes).is_some() + }); + transformer_options.typescript = { let preset_name = "typescript"; if options.has_preset("typescript") { diff --git a/crates/oxc_traverse/src/context/mod.rs b/crates/oxc_traverse/src/context/mod.rs index 7192769577b67..47b940932c1c9 100644 --- a/crates/oxc_traverse/src/context/mod.rs +++ b/crates/oxc_traverse/src/context/mod.rs @@ -284,6 +284,23 @@ impl<'a> TraverseCtx<'a> { self.scoping.insert_scope_below_expression(expr, flags) } + /// Remove scope for an expression from the scope chain. + /// + /// Delete the scope and set parent of its child scopes to its parent scope. + /// e.g.: + /// * Starting scopes parentage `A -> B`, `B -> C`, `B -> D`. + /// * Remove scope `B` from chain. + /// * End result: scopes `A -> C`, `A -> D`. + /// + /// Use this when removing an expression which owns a scope, without removing its children. + /// For example when unwrapping `(() => foo)()` to just `foo`. + /// `foo` here could be an expression which itself contains scopes. + /// + /// This is a shortcut for `ctx.scoping.remove_scope_for_expression`. + pub fn remove_scope_for_expression(&mut self, scope_id: ScopeId, expr: &Expression) { + self.scoping.remove_scope_for_expression(scope_id, expr); + } + /// Generate UID var name. /// /// Finds a unique variable name which does clash with any other variables used in the program. diff --git a/crates/oxc_traverse/src/context/scoping.rs b/crates/oxc_traverse/src/context/scoping.rs index 415a672dd2d46..629b779e5b75e 100644 --- a/crates/oxc_traverse/src/context/scoping.rs +++ b/crates/oxc_traverse/src/context/scoping.rs @@ -139,6 +139,32 @@ impl TraverseScoping { new_scope_id } + /// Remove scope for an expression from the scope chain. + /// + /// Delete the scope and set parent of its child scopes to its parent scope. + /// e.g.: + /// * Starting scopes parentage `A -> B`, `B -> C`, `B -> D`. + /// * Remove scope `B` from chain. + /// * End result: scopes `A -> C`, `A -> D`. + /// + /// Use this when removing an expression which owns a scope, without removing its children. + /// For example when unwrapping `(() => foo)()` to just `foo`. + /// `foo` here could be an expression which itself contains scopes. + pub fn remove_scope_for_expression(&mut self, scope_id: ScopeId, expr: &Expression) { + let mut collector = ChildScopeCollector::new(); + collector.visit_expression(expr); + + let child_ids = collector.scope_ids; + if !child_ids.is_empty() { + let parent_id = self.scopes.get_parent_id(scope_id); + for child_id in child_ids { + self.scopes.set_parent_id(child_id, parent_id); + } + } + + self.scopes.delete_scope(scope_id); + } + /// Generate UID var name. /// /// Finds a unique variable name which does clash with any other variables used in the program. diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index b45f9dca25f86..4faa31262c0e6 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,8 +1,9 @@ commit: d20b314c -Passed: 342/1051 +Passed: 348/1058 # All Passed: +* babel-plugin-transform-class-static-block * babel-plugin-transform-logical-assignment-operators * babel-plugin-transform-optional-catch-binding * babel-preset-react @@ -11,7 +12,7 @@ Passed: 342/1051 * babel-plugin-transform-react-jsx-source -# babel-preset-env (109/585) +# babel-preset-env (108/585) * .plugins-overlapping/chrome-49/input.js x Output mismatch @@ -1437,6 +1438,9 @@ x Output mismatch * shipped-proposals/new-class-features-chrome-90/input.js x Output mismatch +* shipped-proposals/new-class-features-chrome-94/input.js +x Output mismatch + * shipped-proposals/new-class-features-firefox-70/input.js x Output mismatch diff --git a/tasks/transform_conformance/snapshots/babel_exec.snap.md b/tasks/transform_conformance/snapshots/babel_exec.snap.md index ed6e7ec0945ea..05fa232b70119 100644 --- a/tasks/transform_conformance/snapshots/babel_exec.snap.md +++ b/tasks/transform_conformance/snapshots/babel_exec.snap.md @@ -1,8 +1,9 @@ commit: d20b314c -Passed: 34/62 +Passed: 45/73 # All Passed: +* babel-plugin-transform-class-static-block * babel-plugin-transform-logical-assignment-operators * babel-plugin-transform-nullish-coalescing-operator * babel-plugin-transform-optional-catch-binding diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index 7d55b7aebe430..6d32e6444f46e 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,8 +1,9 @@ commit: d20b314c -Passed: 61/70 +Passed: 66/75 # All Passed: +* babel-plugin-transform-class-static-block * babel-plugin-transform-nullish-coalescing-operator * babel-plugin-transform-optional-catch-binding * babel-plugin-transform-exponentiation-operator diff --git a/tasks/transform_conformance/src/constants.rs b/tasks/transform_conformance/src/constants.rs index a22718f51b63b..3455c0160e922 100644 --- a/tasks/transform_conformance/src/constants.rs +++ b/tasks/transform_conformance/src/constants.rs @@ -4,7 +4,7 @@ pub(crate) const PLUGINS: &[&str] = &[ // "babel-plugin-transform-unicode-sets-regex", // // ES2022 // "babel-plugin-transform-class-properties", - // "babel-plugin-transform-class-static-block", + "babel-plugin-transform-class-static-block", // "babel-plugin-transform-private-methods", // "babel-plugin-transform-private-property-in-object", // // [Syntax] "babel-plugin-transform-syntax-top-level-await", diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-assignment/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-assignment/input.js new file mode 100644 index 0000000000000..08ce84e17ad07 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-assignment/input.js @@ -0,0 +1,19 @@ +let a, b, d, e; + +class C { + static { + a = this; + } + static { + [b, c] = this; + } + static { + d ??= this; + } + static { + e.f = this; + } + static { + [g.h, i] = this; + } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-assignment/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-assignment/output.js new file mode 100644 index 0000000000000..04adf9b37af17 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-assignment/output.js @@ -0,0 +1,9 @@ +let a, b, d, e; + +class C { + static #_ = a = this; + static #_2 = [b, c] = this; + static #_3 = d ??= this; + static #_4 = e.f = this; + static #_5 = [g.h, i] = this; +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-identifier/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-identifier/input.js new file mode 100644 index 0000000000000..6f5179b1f30d6 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-identifier/input.js @@ -0,0 +1,8 @@ +class C { + static { + C; + } + static { + x; + } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-identifier/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-identifier/output.js new file mode 100644 index 0000000000000..83c8b6fe699a0 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/contains-identifier/output.js @@ -0,0 +1,4 @@ +class C { + static #_ = C; + static #_2 = x; +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/empty-blocks/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/empty-blocks/input.js new file mode 100644 index 0000000000000..22e23742c1ad6 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/empty-blocks/input.js @@ -0,0 +1,4 @@ +class C { + static {} + static {} +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/empty-blocks/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/empty-blocks/output.js new file mode 100644 index 0000000000000..5785118f4a3f5 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/empty-blocks/output.js @@ -0,0 +1,4 @@ +class C { + static #_ = (() => {})(); + static #_2 = (() => {})(); +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/nested-scopes/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/nested-scopes/input.js new file mode 100644 index 0000000000000..553e9595fd8aa --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/nested-scopes/input.js @@ -0,0 +1,14 @@ +let x, y; + +class C { + static { + x = (() => this)(); + } + + static { + if (true) { + y = this; + z = this; + } + } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/nested-scopes/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/nested-scopes/output.js new file mode 100644 index 0000000000000..bd88f9731ecaa --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/nested-scopes/output.js @@ -0,0 +1,11 @@ +let x, y; + +class C { + static #_ = x = (() => this)(); + static #_2 = (() => { + if (true) { + y = this; + z = this; + } + })(); +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/options.json new file mode 100644 index 0000000000000..a187185300331 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-class-static-block" + ] +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/properties-and-methods/input.ts b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/properties-and-methods/input.ts new file mode 100644 index 0000000000000..e0ba2603420ba --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/properties-and-methods/input.ts @@ -0,0 +1,21 @@ +function foo() {} + +export class C { + // Private properties and methods use up prop names for static block + #_ = 1; + static #_2 = 2; + #_3() {} + static #_4() {} + accessor #_5 = 5; + static accessor #_6 = 6; + + // Non-private don't use up prop names + _7 = 7; + static _8 = 8; + _9() {} + static _10() {} + + static { + foo(); + } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/properties-and-methods/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/properties-and-methods/output.js new file mode 100644 index 0000000000000..df2d886d73785 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-static-block/test/fixtures/properties-and-methods/output.js @@ -0,0 +1,16 @@ +function foo() { } + +export class C { + #_ = 1; + static #_2 = 2; + #_3() {} + static #_4() {} + accessor #_5 = 5; + static accessor #_6 = 6; + _7 = 7; + static _8 = 8; + _9() {} + static _10() {} + + static #_7 = foo(); +}