diff --git a/crates/swc_ecma_transformer/oxc/common/arrow_function_converter.rs b/crates/swc_ecma_transformer/oxc/common/arrow_function_converter.rs new file mode 100644 index 000000000000..1915d0966369 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/arrow_function_converter.rs @@ -0,0 +1,1332 @@ +//! Arrow Functions Converter +//! +//! This converter transforms arrow functions (`() => {}`) to function expressions (`function () {}`). +//! +//! ## Example +//! +//! Input: +//! ```js +//! var a = () => {}; +//! var a = b => b; +//! +//! const double = [1, 2, 3].map(num => num * 2); +//! console.log(double); // [2,4,6] +//! +//! var bob = { +//! name: "Bob", +//! friends: ["Sally", "Tom"], +//! printFriends() { +//! this.friends.forEach(f => console.log(this.name + " knows " + f)); +//! }, +//! }; +//! console.log(bob.printFriends()); +//! ``` +//! +//! Output: +//! ```js +//! var a = function() {}; +//! var a = function(b) { return b; }; +//! +//! const double = [1, 2, 3].map(function(num) { +//! return num * 2; +//! }); +//! console.log(double); // [2,4,6] +//! +//! var bob = { +//! name: "Bob", +//! friends: ["Sally", "Tom"], +//! printFriends() { +//! var _this = this; +//! this.friends.forEach(function(f) { +//! return console.log(_this.name + " knows " + f); +//! }); +//! }, +//! }; +//! console.log(bob.printFriends()); +//! ``` +//! +//! #### Example +//! +//! Using spec mode with the above example produces: +//! +//! ```js +//! var _this = this; +//! +//! var a = function a() { +//! babelHelpers.newArrowCheck(this, _this); +//! }.bind(this); +//! var a = function a(b) { +//! babelHelpers.newArrowCheck(this, _this); +//! return b; +//! }.bind(this); +//! +//! const double = [1, 2, 3].map( +//! function(num) { +//! babelHelpers.newArrowCheck(this, _this); +//! return num * 2; +//! }.bind(this) +//! ); +//! console.log(double); // [2,4,6] +//! +//! var bob = { +//! name: "Bob", +//! friends: ["Sally", "Tom"], +//! printFriends() { +//! var _this2 = this; +//! this.friends.forEach( +//! function(f) { +//! babelHelpers.newArrowCheck(this, _this2); +//! return console.log(this.name + " knows " + f); +//! }.bind(this) +//! ); +//! }, +//! }; +//! console.log(bob.printFriends()); +//! ``` +//! +//! The Implementation based on +//! + +use compact_str::CompactString; +use indexmap::IndexMap; +use rustc_hash::{FxBuildHasher, FxHashSet}; + +use oxc_allocator::{Box as ArenaBox, TakeIn, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_ast_visit::{VisitMut, walk_mut::walk_expression}; +use oxc_data_structures::stack::{NonEmptyStack, SparseStack}; +use oxc_semantic::{ReferenceFlags, SymbolId}; +use oxc_span::{GetSpan, SPAN}; +use oxc_syntax::{ + scope::{ScopeFlags, ScopeId}, + symbol::SymbolFlags, +}; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse}; + +use crate::{EnvOptions, utils::ast_builder::wrap_expression_in_arrow_function_iife}; +use crate::{context::TraverseCtx, state::TransformState}; + +type FxIndexMap = IndexMap; + +/// Mode for arrow function conversion +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArrowFunctionConverterMode { + /// Disable arrow function conversion + Disabled, + + /// Convert all arrow functions to regular functions + Enabled, + + /// Only convert async arrow functions + AsyncOnly, +} + +#[derive(PartialEq, Eq, Hash)] +struct SuperMethodKey<'a> { + /// If it is true, the method should accept a value parameter. + is_assignment: bool, + /// Name of property getter/setter is for. + /// Empty string for computed properties. + property: &'a str, +} + +struct SuperMethodInfo<'a> { + binding: BoundIdentifier<'a>, + super_expr: Expression<'a>, + /// If it is true, the method should accept a prop parameter. + is_computed: bool, +} + +pub struct ArrowFunctionConverter<'a> { + mode: ArrowFunctionConverterMode, + this_var_stack: SparseStack>, + arguments_var_stack: SparseStack>, + constructor_super_stack: NonEmptyStack, + arguments_needs_transform_stack: NonEmptyStack, + renamed_arguments_symbol_ids: FxHashSet, + // TODO(improve-on-babel): `FxHashMap` would suffice here. Iteration order is not important. + // Only using `FxIndexMap` for predictable iteration order to match Babel's output. + super_methods_stack: NonEmptyStack, SuperMethodInfo<'a>>>, + super_needs_transform_stack: NonEmptyStack, +} + +impl ArrowFunctionConverter<'_> { + pub fn new(env: &EnvOptions) -> Self { + let mode = if env.es2015.arrow_function.is_some() { + ArrowFunctionConverterMode::Enabled + } else if env.es2017.async_to_generator || env.es2018.async_generator_functions { + ArrowFunctionConverterMode::AsyncOnly + } else { + ArrowFunctionConverterMode::Disabled + }; + // `SparseStack`s are created with 1 empty entry, for `Program` + Self { + mode, + this_var_stack: SparseStack::new(), + arguments_var_stack: SparseStack::new(), + constructor_super_stack: NonEmptyStack::new(false), + arguments_needs_transform_stack: NonEmptyStack::new(false), + renamed_arguments_symbol_ids: FxHashSet::default(), + super_methods_stack: NonEmptyStack::new(FxIndexMap::default()), + super_needs_transform_stack: NonEmptyStack::new(false), + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ArrowFunctionConverter<'a> { + // Note: No visitors for `TSModuleBlock` because `this` is not legal in TS module blocks. + // + + /// Insert `var _this = this;` for the global scope. + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.is_disabled() { + return; + } + + let this_var = self.this_var_stack.take_last(); + let arguments_var = self.arguments_var_stack.take_last(); + self.insert_variable_statement_at_the_top_of_statements( + program.scope_id(), + &mut program.body, + this_var, + arguments_var, + // `super()` Only allowed in class constructor + None, + ctx, + ); + + debug_assert!(self.this_var_stack.is_exhausted()); + debug_assert!(self.this_var_stack.first().is_none()); + debug_assert!(self.arguments_var_stack.is_exhausted()); + debug_assert!(self.arguments_var_stack.first().is_none()); + debug_assert!(self.constructor_super_stack.is_exhausted()); + // TODO: This assertion currently failing because we don't handle `super` in arrow functions + // in class static properties correctly. + // e.g. `class C { static f = () => super.prop; }` + // debug_assert!(self.constructor_super_stack.first() == &false); + debug_assert!(self.super_methods_stack.is_exhausted()); + debug_assert!(self.super_methods_stack.first().is_empty()); + debug_assert!(self.super_needs_transform_stack.is_exhausted()); + debug_assert!(self.super_needs_transform_stack.first() == &false); + } + + fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if self.is_disabled() || func.body.is_none() { + return; + } + + self.this_var_stack.push(None); + self.arguments_var_stack.push(None); + self.constructor_super_stack.push(false); + + if Self::is_class_method_like_ancestor(ctx.parent()) { + self.super_methods_stack.push(FxIndexMap::default()); + self.super_needs_transform_stack.push(func.r#async); + } + } + + /// ```ts + /// function a(){ + /// return () => console.log(this); + /// } + /// // to + /// function a(){ + /// var _this = this; + /// return function() { return console.log(_this); }; + /// } + /// ``` + /// Insert the var _this = this; statement outside the arrow function + fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if self.is_disabled() { + return; + } + + let scope_id = func.scope_id(); + let Some(body) = &mut func.body else { + return; + }; + let this_var = self.this_var_stack.pop(); + let arguments_var = self.arguments_var_stack.pop(); + let super_methods = Self::is_class_method_like_ancestor(ctx.parent()).then(|| { + self.super_needs_transform_stack.pop(); + self.super_methods_stack.pop() + }); + + self.insert_variable_statement_at_the_top_of_statements( + scope_id, + &mut body.statements, + this_var, + arguments_var, + super_methods, + ctx, + ); + self.constructor_super_stack.pop(); + } + + fn enter_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.is_async_only() { + let previous = *self.arguments_needs_transform_stack.last(); + self.arguments_needs_transform_stack.push(previous || arrow.r#async); + + if Self::in_class_property_definition_value(ctx) { + self.this_var_stack.push(None); + self.super_methods_stack.push(FxIndexMap::default()); + } + self.super_needs_transform_stack + .push(arrow.r#async || *self.super_needs_transform_stack.last()); + } + } + + fn exit_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.is_async_only() { + if Self::in_class_property_definition_value(ctx) { + let this_var = self.this_var_stack.pop(); + let super_methods = self.super_methods_stack.pop(); + self.insert_variable_statement_at_the_top_of_statements( + arrow.scope_id(), + &mut arrow.body.statements, + this_var, + None, + Some(super_methods), + ctx, + ); + } + + self.super_needs_transform_stack.pop(); + } + } + + fn enter_function_body(&mut self, _body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) { + if self.is_async_only() { + // Ignore arrow functions + if let Ancestor::FunctionBody(func) = ctx.parent() { + let is_async_method = + *func.r#async() && Self::is_class_method_like_ancestor(ctx.ancestor(1)); + self.arguments_needs_transform_stack.push(is_async_method); + } + } + } + + fn exit_function_body(&mut self, _body: &mut FunctionBody<'a>, _ctx: &mut TraverseCtx<'a>) { + // This covers exiting either a `Function` or an `ArrowFunctionExpression` + if self.is_async_only() { + self.arguments_needs_transform_stack.pop(); + } + } + + fn enter_static_block(&mut self, _block: &mut StaticBlock<'a>, _ctx: &mut TraverseCtx<'a>) { + if self.is_disabled() { + return; + } + + self.this_var_stack.push(None); + self.super_methods_stack.push(FxIndexMap::default()); + } + + fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + if self.is_disabled() { + return; + } + + let this_var = self.this_var_stack.pop(); + let super_methods = self.super_methods_stack.pop(); + self.insert_variable_statement_at_the_top_of_statements( + block.scope_id(), + &mut block.body, + this_var, + // `arguments` is not allowed to be used in static blocks + None, + Some(super_methods), + ctx, + ); + } + + fn enter_jsx_element_name( + &mut self, + element_name: &mut JSXElementName<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.is_disabled() { + return; + } + + if let JSXElementName::ThisExpression(this) = element_name + && let Some(ident) = self.get_this_identifier(this.span, ctx) + { + *element_name = JSXElementName::IdentifierReference(ident); + } + } + + fn enter_jsx_member_expression_object( + &mut self, + object: &mut JSXMemberExpressionObject<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.is_disabled() { + return; + } + + if let JSXMemberExpressionObject::ThisExpression(this) = object + && let Some(ident) = self.get_this_identifier(this.span, ctx) + { + *object = JSXMemberExpressionObject::IdentifierReference(ident); + } + } + + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.is_disabled() { + return; + } + + let new_expr = match expr { + Expression::ThisExpression(this) => { + self.get_this_identifier(this.span, ctx).map(Expression::Identifier) + } + Expression::Super(_) => { + *self.constructor_super_stack.last_mut() = true; + return; + } + Expression::CallExpression(call) => self.transform_call_expression_for_super(call, ctx), + Expression::AssignmentExpression(assignment) => { + self.transform_assignment_expression_for_super(assignment, ctx) + } + match_member_expression!(Expression) => { + self.transform_member_expression_for_super(expr, None, ctx) + } + Expression::ArrowFunctionExpression(arrow) => { + // TODO: If the async arrow function without `this` or `super` usage, we can skip this step. + if self.is_async_only() + && arrow.r#async + && Self::in_class_property_definition_value(ctx) + { + // Inside class property definition value, since async arrow function will be + // converted to a generator function by `AsyncToGenerator` plugin, ensure + // `_this = this` and `super` methods are inserted correctly. We need to + // wrap the async arrow function with an normal arrow function IIFE. + // + // ```js + // class A { + // prop = async () => {} + // } + // // to + // class A { + // prop = (() => { return async () => {} })(); + // } + // ``` + Some(wrap_expression_in_arrow_function_iife(expr.take_in(ctx.ast), ctx)) + } else { + return; + } + } + _ => return, + }; + + if let Some(new_expr) = new_expr { + *expr = new_expr; + } + } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.is_disabled() { + return; + } + + if let Expression::ArrowFunctionExpression(arrow_function_expr) = expr { + // TODO: Here should return early as long as the async-to-generator plugin is enabled, + // but currently we don't know which plugin is enabled. + if self.is_async_only() || arrow_function_expr.r#async { + return; + } + + let Expression::ArrowFunctionExpression(arrow_function_expr) = expr.take_in(ctx.ast) + else { + unreachable!() + }; + + *expr = Self::transform_arrow_function_expression(arrow_function_expr, ctx); + } + } + + // `#[inline]` because this is a hot path + #[inline] + fn enter_identifier_reference( + &mut self, + ident: &mut IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Do this check here rather than in `transform_identifier_reference_for_arguments` + // so that the fast path for "no transform required" doesn't require a function call + let arguments_needs_transform = *self.arguments_needs_transform_stack.last(); + if arguments_needs_transform { + self.transform_identifier_reference_for_arguments(ident, ctx); + } + } + + // `#[inline]` because this is a hot path + #[inline] + fn enter_binding_identifier( + &mut self, + ident: &mut BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Do this check here rather than in `transform_binding_identifier_for_arguments` + // so that the fast path for "no transform required" doesn't require a function call + let arguments_needs_transform = *self.arguments_needs_transform_stack.last(); + if arguments_needs_transform { + self.transform_binding_identifier_for_arguments(ident, ctx); + } + } +} + +impl<'a> ArrowFunctionConverter<'a> { + /// Check if arrow function conversion is disabled + fn is_disabled(&self) -> bool { + self.mode == ArrowFunctionConverterMode::Disabled + } + + /// Check if arrow function conversion has enabled for transform async arrow functions + #[inline] + fn is_async_only(&self) -> bool { + self.mode == ArrowFunctionConverterMode::AsyncOnly + } + + fn get_this_identifier( + &mut self, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Option>> { + // Find arrow function we are currently in (if we are) + let arrow_scope_id = self.get_scope_id_from_this_affected_block(ctx)?; + + // TODO(improve-on-babel): We create a new UID for every scope. This is pointless, as only one + // `this` can be in scope at a time. We could create a single `_this` UID and reuse it in each + // scope. But this does not match output for some of Babel's test cases. + // + let this_var = self.this_var_stack.last_or_init(|| { + let target_scope_id = ctx + .scoping() + .scope_ancestors(arrow_scope_id) + // Skip arrow function scope + .skip(1) + .find(|&scope_id| { + let scope_flags = ctx.scoping().scope_flags(scope_id); + scope_flags.intersects( + ScopeFlags::Function | ScopeFlags::Top | ScopeFlags::ClassStaticBlock, + ) && !scope_flags.contains(ScopeFlags::Arrow) + }) + .unwrap(); + ctx.generate_uid("this", target_scope_id, SymbolFlags::FunctionScopedVariable) + }); + // TODO: Add `BoundIdentifier::create_spanned_read_reference_boxed` method (and friends) + // for this use case, so we can avoid `alloc()` call here. + // I (@overlookmotel) doubt it'd make a perf difference, but it'd be cleaner code. + Some(ctx.ast.alloc(this_var.create_spanned_read_reference(span, ctx))) + } + + /// Traverses upward through ancestor nodes to find the `ScopeId` of the block + /// that potential affects the `this` expression. + fn get_scope_id_from_this_affected_block(&self, ctx: &TraverseCtx<'a>) -> Option { + // `this` inside a class resolves to `this` *outside* the class in: + // * `extends` clause + // * Computed method key + // * Computed property key + // * Computed accessor property key (but `this` in this position is not legal TS) + // + // ```js + // // All these `this` refer to global `this` + // class C extends this { + // [this] = 123; + // static [this] = 123; + // [this]() {} + // static [this]() {} + // accessor [this] = 123; + // static accessor [this] = 123; + // } + // ``` + // + // `this` resolves to the class / class instance (i.e. `this` defined *within* the class) in: + // * Method body + // * Method param + // * Property value + // * Static block + // + // ```js + // // All these `this` refer to `this` defined within the class + // class C { + // a = this; + // static b = this; + // #c = this; + // d() { this } + // static e() { this } + // #f() { this } + // g(x = this) {} + // accessor h = this; + // static accessor i = this; + // static { this } + // } + // ``` + // + // So in this loop, we only exit when we encounter one of the above. + let mut ancestors = ctx.ancestors(); + while let Some(ancestor) = ancestors.next() { + match ancestor { + // Top level + Ancestor::ProgramBody(_) + // Function params + | Ancestor::FunctionParams(_) + // Class property body + | Ancestor::PropertyDefinitionValue(_) + // Class accessor property body + | Ancestor::AccessorPropertyValue(_) + // Class static block + | Ancestor::StaticBlockBody(_) => return None, + // Arrow function + Ancestor::ArrowFunctionExpressionParams(func) => { + return if self.is_async_only() && !*func.r#async() { + // Continue checking the parent to see if it's inside an async function. + continue; + } else { + Some(func.scope_id().get().unwrap()) + }; + } + Ancestor::ArrowFunctionExpressionBody(func) => { + return if self.is_async_only() && !*func.r#async() { + // Continue checking the parent to see if it's inside an async function. + continue; + } else { + Some(func.scope_id().get().unwrap()) + }; + } + // Function body (includes class method or object method) + Ancestor::FunctionBody(func) => { + // If we're inside a class async method or an object async method, and `is_async_only` is true, + // the `AsyncToGenerator` or `AsyncGeneratorFunctions` plugin will move the body + // of the method into a new generator function. This transformation can cause `this` + // to point to the wrong context. + // To prevent this issue, we replace `this` with `_this`, treating it similarly + // to how we handle arrow functions. Therefore, we return the `ScopeId` of the function. + return if self.is_async_only() + && *func.r#async() + && Self::is_class_method_like_ancestor( + ancestors.next().unwrap() + ) { + Some(func.scope_id().get().unwrap()) + } else { + None + }; + } + _ => {} + } + } + unreachable!(); + } + + fn transform_arrow_function_expression( + arrow_function_expr: ArenaBox<'a, ArrowFunctionExpression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let arrow_function_expr = arrow_function_expr.unbox(); + let scope_id = arrow_function_expr.scope_id(); + let flags = ctx.scoping_mut().scope_flags_mut(scope_id); + *flags &= !ScopeFlags::Arrow; + + let mut body = arrow_function_expr.body; + + if arrow_function_expr.expression { + assert!(body.statements.len() == 1); + let stmt = body.statements.pop().unwrap(); + let Statement::ExpressionStatement(stmt) = stmt else { unreachable!() }; + let stmt = stmt.unbox(); + let return_statement = ctx.ast.statement_return(stmt.span, Some(stmt.expression)); + body.statements.push(return_statement); + } + + ctx.ast.expression_function_with_scope_id_and_pure_and_pife( + arrow_function_expr.span, + FunctionType::FunctionExpression, + None, + false, + arrow_function_expr.r#async, + false, + arrow_function_expr.type_parameters, + NONE, + arrow_function_expr.params, + arrow_function_expr.return_type, + Some(body), + scope_id, + false, + false, + ) + } + + /// Check whether the given [`Ancestor`] is a class method-like node. + fn is_class_method_like_ancestor(ancestor: Ancestor) -> bool { + match ancestor { + // `class A { async foo() {} }` + Ancestor::MethodDefinitionValue(_) => true, + // Only `({ async foo() {} })` does not include non-method like `({ foo: async function() {} })`, + // because it's just a property with a function value + Ancestor::ObjectPropertyValue(property) => *property.method(), + _ => false, + } + } + + /// Check whether currently in a class property initializer. + /// e.g. `x` in `class C { prop = [foo(x)]; }` + fn in_class_property_definition_value(ctx: &TraverseCtx<'a>) -> bool { + for ancestor in ctx.ancestors() { + if ancestor.is_parent_of_statement() { + return false; + } else if matches!(ancestor, Ancestor::PropertyDefinitionValue(_)) { + return true; + } + } + unreachable!() + } + + /// Transforms a `MemberExpression` whose object is a `super` expression. + /// + /// In the [`AsyncToGenerator`](crate::es2017::AsyncToGenerator) and + /// [`AsyncGeneratorFunctions`](crate::es2018::AsyncGeneratorFunctions) plugins, + /// we move the body of an async method to a new generator function. This can cause + /// `super` expressions to appear in unexpected places, leading to syntax errors. + /// + /// ## How it works + /// + /// To correctly handle `super` expressions, we need to ensure that they remain + /// within the async method's body. + /// + /// This function modifies the `super` expression to call a new arrow function + /// whose body includes the original `super` expression. The arrow function's name + /// is generated based on the property name, such as `_superprop_getProperty`. + /// + /// The `super` expressions are temporarily stored in [`Self::super_methods_stack`] + /// and eventually inserted by [`Self::insert_variable_statement_at_the_top_of_statements`].` + /// + /// ## Example + /// + /// Before: + /// ```js + /// super.property; + /// super['property'] + /// ``` + /// + /// After: + /// ```js + /// var _superprop_getProperty = () => super.property, _superprop_get = (_prop) => super[_prop]; + /// _superprop_getProperty(); + /// _superprop_get('property') + /// ``` + fn transform_member_expression_for_super( + &mut self, + expr: &mut Expression<'a>, + assign_value: Option<&mut Expression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if !*self.super_needs_transform_stack.last() { + return None; + } + + let super_methods = self.super_methods_stack.last_mut(); + + let mut argument = None; + let mut property = ""; + let init = match expr.to_member_expression_mut() { + MemberExpression::ComputedMemberExpression(computed_member) => { + if !computed_member.object.is_super() { + return None; + } + + // The property will as a parameter to pass to the new arrow function. + // `super[property]` to `_superprop_get(property)` + argument = Some(computed_member.expression.take_in(ctx.ast)); + computed_member.object.take_in(ctx.ast) + } + MemberExpression::StaticMemberExpression(static_member) => { + if !static_member.object.is_super() { + return None; + } + + // Used to generate the name of the arrow function. + property = static_member.property.name.as_str(); + expr.take_in(ctx.ast) + } + MemberExpression::PrivateFieldExpression(_) => { + // Private fields can't be accessed by `super`. + return None; + } + }; + + let is_assignment = assign_value.is_some(); + let key = SuperMethodKey { is_assignment, property }; + let super_info = super_methods.entry(key).or_insert_with(|| { + let binding_name = Self::generate_super_binding_name(is_assignment, property); + let binding = ctx + .generate_uid_in_current_scope(&binding_name, SymbolFlags::FunctionScopedVariable); + SuperMethodInfo { binding, super_expr: init, is_computed: argument.is_some() } + }); + + let callee = super_info.binding.create_read_expression(ctx); + let mut arguments = ctx.ast.vec_with_capacity( + usize::from(assign_value.is_some()) + usize::from(argument.is_some()), + ); + // _prop + if let Some(argument) = argument { + arguments.push(Argument::from(argument)); + } + // _value + if let Some(assign_value) = assign_value { + arguments.push(Argument::from(assign_value.take_in(ctx.ast))); + } + let call = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false); + Some(call) + } + + /// Transform a `CallExpression` whose callee is a `super` member expression. + /// + /// This function modifies calls to `super` methods within arrow functions + /// to ensure the correct `this` context is maintained after transformation. + /// + /// ## Example + /// + /// Before: + /// ```js + /// super.method(a, b); + /// ``` + /// + /// After: + /// ```js + /// var _superprop_getMethod = () => super.method; + /// _superprop_getMethod.call(this, a, b); + /// ``` + #[inline] + fn transform_call_expression_for_super( + &mut self, + call: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if !*self.super_needs_transform_stack.last() || !call.callee.is_member_expression() { + return None; + } + + let object = self.transform_member_expression_for_super(&mut call.callee, None, ctx)?; + // Add `this` as the first argument and original arguments as the rest. + let mut arguments = ctx.ast.vec_with_capacity(call.arguments.len() + 1); + arguments.push(Argument::from(ctx.ast.expression_this(SPAN))); + arguments.extend(call.arguments.take_in(ctx.ast)); + + let property = ctx.ast.identifier_name(SPAN, "call"); + let callee = ctx.ast.member_expression_static(SPAN, object, property, false); + let callee = Expression::from(callee); + Some(ctx.ast.expression_call(SPAN, callee, NONE, arguments, false)) + } + + /// Transform an `AssignmentExpression` whose assignment target is a `super` member expression. + /// + /// In this function, we replace assignments to call a new arrow function whose body includes + /// [AssignmentExpression::left], and use [AssignmentExpression::right] as arguments for the call expression. + /// + /// ## Example + /// + /// Before: + /// ```js + /// super.value = true; + /// ``` + /// + /// After: + /// ```js + /// var _superprop_setValue = (_value) => super.value = _value; + /// _superprop_setValue(true); + /// ``` + fn transform_assignment_expression_for_super( + &mut self, + assignment: &mut AssignmentExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // Check if the left of the assignment is a `super` member expression. + if !*self.super_needs_transform_stack.last() + || !assignment.left.as_member_expression().is_some_and(|m| m.object().is_super()) + { + return None; + } + + let assignment_target = assignment.left.take_in(ctx.ast); + let mut assignment_expr = Expression::from(assignment_target.into_member_expression()); + self.transform_member_expression_for_super( + &mut assignment_expr, + Some(&mut assignment.right), + ctx, + ) + } + + /// Adjust the scope of the binding. + /// + /// Since scope can be moved or deleted, we need to ensure the scope of the binding + /// same as the target scope, if it's mismatch, we need to move the binding to the target scope. + fn adjust_binding_scope( + target_scope_id: ScopeId, + binding: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let original_scope_id = ctx.scoping().symbol_scope_id(binding.symbol_id); + if target_scope_id != original_scope_id { + ctx.scoping_mut().set_symbol_scope_id(binding.symbol_id, target_scope_id); + ctx.scoping_mut().move_binding(original_scope_id, target_scope_id, &binding.name); + } + } + + /// Generate a variable declarator for the super method by the given [`SuperMethodInfo`]. + fn generate_super_method( + target_scope_id: ScopeId, + super_method: SuperMethodInfo<'a>, + is_assignment: bool, + ctx: &mut TraverseCtx<'a>, + ) -> VariableDeclarator<'a> { + let SuperMethodInfo { binding, super_expr: mut init, is_computed } = super_method; + + Self::adjust_binding_scope(target_scope_id, &binding, ctx); + let scope_id = + ctx.create_child_scope(target_scope_id, ScopeFlags::Arrow | ScopeFlags::Function); + + let mut items = + ctx.ast.vec_with_capacity(usize::from(is_computed) + usize::from(is_assignment)); + + // Create a parameter for the prop if it's a computed member expression. + if is_computed { + // TODO(improve-on-babel): No need for UID here. Just `prop` would be fine as there's nothing + // in `prop => super[prop]` or `(prop, value) => super[prop] = value` which can clash. + let param_binding = + ctx.generate_uid("prop", scope_id, SymbolFlags::FunctionScopedVariable); + let param = ctx.ast.formal_parameter( + SPAN, + ctx.ast.vec(), + param_binding.create_binding_pattern(ctx), + None, + false, + false, + ); + items.push(param); + + // `super` -> `super[prop]` + init = Expression::from(ctx.ast.member_expression_computed( + SPAN, + init, + param_binding.create_read_expression(ctx), + false, + )); + } + + // Create a parameter for the value if it's an assignment. + if is_assignment { + // TODO(improve-on-babel): No need for UID here. Just `value` would be fine as there's nothing + // in `value => super.prop = value` or `(prop, value) => super[prop] = value` which can clash. + let param_binding = + ctx.generate_uid("value", scope_id, SymbolFlags::FunctionScopedVariable); + let param = ctx.ast.formal_parameter( + SPAN, + ctx.ast.vec(), + param_binding.create_binding_pattern(ctx), + None, + false, + false, + ); + items.push(param); + + // `super[prop]` -> `super[prop] = value` + let left = SimpleAssignmentTarget::from(init.into_member_expression()); + let left = AssignmentTarget::from(left); + let right = param_binding.create_read_expression(ctx); + init = ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, left, right); + } + + let params = ctx.ast.formal_parameters( + SPAN, + FormalParameterKind::ArrowFormalParameters, + items, + NONE, + ); + let statements = ctx.ast.vec1(ctx.ast.statement_expression(SPAN, init)); + let body = ctx.ast.function_body(SPAN, ctx.ast.vec(), statements); + let init = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife( + SPAN, true, false, NONE, params, NONE, body, scope_id, false, false, + ); + ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + binding.create_binding_pattern(ctx), + Some(init), + false, + ) + } + + /// Generate a binding name for the super method, like `superprop_getXXX`. + fn generate_super_binding_name(is_assignment: bool, property: &str) -> CompactString { + let mut name = if is_assignment { + CompactString::const_new("superprop_set") + } else { + CompactString::const_new("superprop_get") + }; + + let Some(&first_byte) = property.as_bytes().first() else { + return name; + }; + + // Capitalize the first letter of the property name. + // Fast path for ASCII (very common case). + // TODO(improve-on-babel): We could just use format `superprop_get_prop` and avoid capitalizing. + if first_byte.is_ascii() { + // We know `IdentifierName`s begin with `a-z`, `A-Z`, `_` or `$` if ASCII, + // so can use a slightly cheaper conversion than `u8::to_ascii_uppercase`. + // Adapted from `u8::to_ascii_uppercase`'s implementation. + // https://godbolt.org/z/5Txa6Pv9z + #[inline] + fn ascii_ident_first_char_uppercase(b: u8) -> u8 { + const ASCII_CASE_MASK: u8 = 0b0010_0000; + let is_lower_case = b >= b'a'; + b ^ (u8::from(is_lower_case) * ASCII_CASE_MASK) + } + + name.push(ascii_ident_first_char_uppercase(first_byte) as char); + if property.len() > 1 { + name.push_str(&property[1..]); + } + } else { + #[cold] + #[inline(never)] + fn push_unicode(property: &str, name: &mut CompactString) { + let mut chars = property.chars(); + let first_char = chars.next().unwrap(); + name.extend(first_char.to_uppercase()); + name.push_str(chars.as_str()); + } + push_unicode(property, &mut name); + } + + name + } + + /// Rename the `arguments` symbol to a new name. + fn rename_arguments_symbol(symbol_id: SymbolId, name: Atom<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = ctx.scoping().symbol_scope_id(symbol_id); + ctx.scoping_mut().rename_symbol(symbol_id, scope_id, name.as_str()); + } + + /// Transform the identifier reference for `arguments` if it's affected after transformation. + /// + /// See [`Self::transform_member_expression_for_super`] for the reason. + fn transform_identifier_reference_for_arguments( + &mut self, + ident: &mut IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if &ident.name != "arguments" { + return; + } + + let reference_id = ident.reference_id(); + let symbol_id = ctx.scoping().get_reference(reference_id).symbol_id(); + + let binding = self.arguments_var_stack.last_or_init(|| { + if let Some(symbol_id) = symbol_id { + let arguments_name = ctx.generate_uid_name("arguments"); + Self::rename_arguments_symbol(symbol_id, arguments_name, ctx); + // Record the symbol ID as a renamed `arguments` variable. + self.renamed_arguments_symbol_ids.insert(symbol_id); + BoundIdentifier::new(arguments_name, symbol_id) + } else { + // We cannot determine the final scope ID of the `arguments` variable insertion, + // because the `arguments` variable will be inserted to a new scope which haven't been created yet, + // so we temporary use root scope id as the fake target scope ID. + let target_scope_id = ctx.scoping().root_scope_id(); + ctx.generate_uid("arguments", target_scope_id, SymbolFlags::FunctionScopedVariable) + } + }); + + // If no symbol ID, it means there is no variable named `arguments` in the scope. + // The following code is just to sync semantics. + if symbol_id.is_none() { + let reference = ctx.scoping_mut().get_reference_mut(reference_id); + reference.set_symbol_id(binding.symbol_id); + ctx.scoping_mut().delete_root_unresolved_reference(&ident.name, reference_id); + ctx.scoping_mut().add_resolved_reference(binding.symbol_id, reference_id); + } + + ident.name = binding.name; + } + + /// Transform the binding identifier for `arguments` if it's affected after transformation. + /// + /// The main work is to rename the `arguments` binding identifier to a new name. + fn transform_binding_identifier_for_arguments( + &mut self, + ident: &mut BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // `arguments` is not allowed to be defined in strict mode. + // Check if strict mode first to avoid the more expensive string comparison check if possible. + if ctx.current_scope_flags().is_strict_mode() || &ident.name != "arguments" { + return; + } + + self.arguments_var_stack.last_or_init(|| { + let arguments_name = ctx.generate_uid_name("arguments"); + ident.name = arguments_name; + let symbol_id = ident.symbol_id(); + Self::rename_arguments_symbol(symbol_id, arguments_name, ctx); + // Record the symbol ID as a renamed `arguments` variable. + self.renamed_arguments_symbol_ids.insert(symbol_id); + BoundIdentifier::new(ident.name, symbol_id) + }); + } + + /// Create a variable declarator looks like `_arguments = arguments;`. + fn create_arguments_var_declarator( + &self, + target_scope_id: ScopeId, + arguments_var: Option>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let arguments_var = arguments_var?; + + // Just a renamed `arguments` variable, we don't need to create a new variable declaration. + if self.renamed_arguments_symbol_ids.contains(&arguments_var.symbol_id) { + return None; + } + + Self::adjust_binding_scope(target_scope_id, &arguments_var, ctx); + + let mut init = + ctx.create_unbound_ident_expr(SPAN, Atom::from("arguments"), ReferenceFlags::Read); + + // Top level may not have `arguments`, so we need to check it. + // `typeof arguments === "undefined" ? void 0 : arguments;` + if ctx.scoping().root_scope_id() == target_scope_id { + let argument = + ctx.create_unbound_ident_expr(SPAN, Atom::from("arguments"), ReferenceFlags::Read); + let typeof_arguments = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, argument); + let undefined_literal = ctx.ast.expression_string_literal(SPAN, "undefined", None); + let test = ctx.ast.expression_binary( + SPAN, + typeof_arguments, + BinaryOperator::StrictEquality, + undefined_literal, + ); + init = ctx.ast.expression_conditional(SPAN, test, ctx.ast.void_0(SPAN), init); + } + + Some(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + arguments_var.create_binding_pattern(ctx), + Some(init), + false, + )) + } + + /// Insert variable statement at the top of the statements. + fn insert_variable_statement_at_the_top_of_statements( + &self, + target_scope_id: ScopeId, + statements: &mut ArenaVec<'a, Statement<'a>>, + this_var: Option>, + arguments_var: Option>, + super_methods: Option>>, + ctx: &mut TraverseCtx<'a>, + ) { + // `_arguments = arguments;` + let arguments = self.create_arguments_var_declarator(target_scope_id, arguments_var, ctx); + + let super_method_count = super_methods.as_ref().map_or(0, FxIndexMap::len); + let declarations_count = + usize::from(arguments.is_some()) + super_method_count + usize::from(this_var.is_some()); + + // Exit if no declarations to be inserted + if declarations_count == 0 { + return; + } + + let mut declarations = ctx.ast.vec_with_capacity(declarations_count); + + if let Some(arguments) = arguments { + declarations.push(arguments); + } + + // `_superprop_getSomething = () => super.something;` + // `_superprop_setSomething = _value => super.something = _value;` + // `_superprop_set = (_prop, _value) => super[_prop] = _value;` + if let Some(super_methods) = super_methods { + declarations.extend(super_methods.into_iter().map(|(key, super_method)| { + Self::generate_super_method(target_scope_id, super_method, key.is_assignment, ctx) + })); + } + + // `_this = this;` + if let Some(this_var) = this_var { + let is_constructor = ctx.scoping().scope_flags(target_scope_id).is_constructor(); + let init = if is_constructor && *self.constructor_super_stack.last() { + // `super()` is called in the constructor body, so we need to insert `_this = this;` + // after `super()` call. Because `this` is not available before `super()` call. + ConstructorBodyThisAfterSuperInserter::new(&this_var, ctx) + .visit_statements(statements); + None + } else { + Some(ctx.ast.expression_this(SPAN)) + }; + Self::adjust_binding_scope(target_scope_id, &this_var, ctx); + let variable_declarator = ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + this_var.create_binding_pattern(ctx), + init, + false, + ); + declarations.push(variable_declarator); + } + + debug_assert_eq!(declarations_count, declarations.len()); + + let stmt = ctx.ast.alloc_variable_declaration( + SPAN, + VariableDeclarationKind::Var, + declarations, + false, + ); + + let stmt = Statement::VariableDeclaration(stmt); + + statements.insert(0, stmt); + } +} + +/// Visitor for inserting `this` after `super` in constructor body. +struct ConstructorBodyThisAfterSuperInserter<'a, 'v> { + this_var_binding: &'v BoundIdentifier<'a>, + ctx: &'v mut TraverseCtx<'a>, +} + +impl<'a, 'v> ConstructorBodyThisAfterSuperInserter<'a, 'v> { + fn new(this_var_binding: &'v BoundIdentifier<'a>, ctx: &'v mut TraverseCtx<'a>) -> Self { + Self { this_var_binding, ctx } + } +} + +impl<'a> VisitMut<'a> for ConstructorBodyThisAfterSuperInserter<'a, '_> { + fn visit_class(&mut self, class: &mut Class<'a>) { + // Only need to transform `super()` in: + // + // 1. Class decorators + // 2. Class `extends` clause + // 3. Class property decorators and computed key + // 4. Class method decorators and computed key + // 5. Class accessor decorators and computed key + // + // Because the `super()` points to the parent class, not the current class. + + // `@(super()) class Inner {}` + // ^^^^^^^ + self.visit_decorators(&mut class.decorators); + + // `class Inner extends super() {}` + // ^^^^^^^ + if let Some(super_class) = &mut class.super_class { + self.visit_expression(super_class); + } + + for element in &mut class.body.body { + match element { + // `class Inner { @(super()) [super()]() {} }` + // ^^^^^^^ ^^^^^^^ + ClassElement::MethodDefinition(method) if method.computed => { + self.visit_decorators(&mut method.decorators); + self.visit_property_key(&mut method.key); + } + // `class Inner { @(super()) [super()] = 123; }` + // ^^^^^^^ ^^^^^^^ + ClassElement::PropertyDefinition(prop) if prop.computed => { + self.visit_decorators(&mut prop.decorators); + self.visit_property_key(&mut prop.key); + } + // `class Inner { @(super()) accessor [super()] = 123; }` + // ^^^^^^^ ^^^^^^^ + ClassElement::AccessorProperty(prop) if prop.computed => { + self.visit_decorators(&mut prop.decorators); + self.visit_property_key(&mut prop.key); + } + _ => {} + } + } + } + + #[inline] // `#[inline]` because is a no-op + fn visit_function(&mut self, _func: &mut Function<'a>, _flags: ScopeFlags) { + // `super()` can't appear in a nested function + } + + /// `super();` -> `super(); _this = this;` + fn visit_statements(&mut self, statements: &mut ArenaVec<'a, Statement<'a>>) { + for (index, stmt) in statements.iter_mut().enumerate() { + if let Statement::ExpressionStatement(expr_stmt) = stmt + && let Expression::CallExpression(call_expr) = &mut expr_stmt.expression + && matches!(&call_expr.callee, Expression::Super(_)) + { + // Visit arguments in `super(x, y, z)` call. + // Required to handle edge case `super(super(), f = () => this)`. + self.visit_arguments(&mut call_expr.arguments); + + // Insert `_this = this;` after `super();` + let assignment = self.create_assignment_to_this_temp_var(); + let assignment = self.ctx.ast.statement_expression(SPAN, assignment); + statements.insert(index + 1, assignment); + + // `super();` found as top-level statement in this block of statements. + // No need to continue visiting later statements, because `_this` is definitely + // assigned to at this point - no need to assign to it again. + // This means we don't visit the whole constructor in the common case where + // `super();` appears as a top-level statement early in class constructor + // `constructor() { super(); blah; blah; blah; }`. + break; + } + + self.visit_statement(stmt); + } + } + + /// `const A = super()` -> `const A = (super(), _this = this);` + // `#[inline]` to avoid a function call for all `Expressions` which are not `super()` (vast majority) + #[inline] + fn visit_expression(&mut self, expr: &mut Expression<'a>) { + if expr.is_super_call_expression() { + self.transform_super_call_expression(expr); + } else { + walk_expression(self, expr); + } + } +} + +impl<'a> ConstructorBodyThisAfterSuperInserter<'a, '_> { + /// `super()` -> `(super(), _this = this)` + fn transform_super_call_expression(&mut self, expr: &mut Expression<'a>) { + let assignment = self.create_assignment_to_this_temp_var(); + let span = expr.span(); + let exprs = self.ctx.ast.vec_from_array([expr.take_in(self.ctx.ast), assignment]); + *expr = self.ctx.ast.expression_sequence(span, exprs); + } + + /// `_this = this` + fn create_assignment_to_this_temp_var(&mut self) -> Expression<'a> { + self.ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + self.this_var_binding.create_write_target(self.ctx), + self.ctx.ast.expression_this(SPAN), + ) + } +} diff --git a/crates/swc_ecma_transformer/oxc/common/computed_key.rs b/crates/swc_ecma_transformer/oxc/common/computed_key.rs new file mode 100644 index 000000000000..388b759f89cb --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/computed_key.rs @@ -0,0 +1,81 @@ +//! Utilities to computed key expressions. + +use oxc_ast::ast::Expression; +use oxc_semantic::SymbolFlags; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + utils::ast_builder::create_assignment, +}; + +impl<'a> TransformCtx<'a> { + /// Check if temp var is required for `key`. + /// + /// `this` does not have side effects, but in this context, it needs a temp var anyway, because `this` + /// in computed key and `this` within class constructor resolve to different `this` bindings. + /// So we need to create a temp var outside of the class to get the correct `this`. + /// `class C { [this] = 1; }` + /// -> `let _this; _this = this; class C { constructor() { this[_this] = 1; } }` + // + // TODO(improve-on-babel): Can avoid the temp var if key is for a static prop/method, + // as in that case the usage of `this` stays outside the class. + #[expect(clippy::unused_self)] // Taking `self` makes it easier to use. + pub fn key_needs_temp_var(&self, key: &Expression, ctx: &TraverseCtx) -> bool { + match key { + // Literals cannot have side effects. + // e.g. `let x = 'x'; class C { [x] = 1; }` or `class C { ['x'] = 1; }`. + Expression::BooleanLiteral(_) + | Expression::NullLiteral(_) + | Expression::NumericLiteral(_) + | Expression::BigIntLiteral(_) + | Expression::RegExpLiteral(_) + | Expression::StringLiteral(_) => false, + // Template literal cannot have side effects if it has no expressions. + // If it *does* have expressions, but they're all literals, then also cannot have side effects, + // but don't bother checking for that as it shouldn't occur in real world code. + // Why would you write "`x${9}z`" when you can just write "`x9z`"? + // Note: "`x${foo}`" *can* have side effects if `foo` is an object with a `toString` method. + Expression::TemplateLiteral(lit) => !lit.expressions.is_empty(), + // `IdentifierReference`s can have side effects if is unbound. + // + // If var is mutated, it also needs a temp var, because of cases like + // `let x = 1; class { [x] = 1; [++x] = 2; }` + // `++x` is hoisted to before class in output, so `x` in 1st key would get the wrong value + // unless it's hoisted out too. + // + // TODO: Add an exec test for this odd case. + // TODO(improve-on-babel): That case is rare. + // Test for it in first pass over class elements, and avoid temp vars where possible. + Expression::Identifier(ident) => { + match ctx.scoping().get_reference(ident.reference_id()).symbol_id() { + Some(symbol_id) => ctx.scoping().symbol_is_mutated(symbol_id), + None => true, + } + } + // Treat any other expression as possibly having side effects e.g. `foo()`. + // TODO: Do fuller analysis to detect expressions which cannot have side effects. + // e.g. `"x" + "y"`. + _ => true, + } + } + + /// Create `let _x;` statement and insert it. + /// Return `_x = x()` assignment, and `_x` identifier referencing same temp var. + pub fn create_computed_key_temp_var( + &self, + key: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (/* assignment */ Expression<'a>, /* identifier */ Expression<'a>) { + let outer_scope_id = ctx.current_block_scope_id(); + // TODO: Handle if is a class expression defined in a function's params. + let binding = + ctx.generate_uid_based_on_node(&key, outer_scope_id, SymbolFlags::BlockScopedVariable); + + self.var_declarations.insert_let(&binding, None, ctx); + + let assignment = create_assignment(&binding, key, ctx); + let ident = binding.create_read_expression(ctx); + + (assignment, ident) + } +} diff --git a/crates/swc_ecma_transformer/oxc/common/duplicate.rs b/crates/swc_ecma_transformer/oxc/common/duplicate.rs new file mode 100644 index 000000000000..28521e277004 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/duplicate.rs @@ -0,0 +1,174 @@ +//! Utilities to duplicate expressions. + +use std::{ + mem::{ManuallyDrop, MaybeUninit}, + ptr, +}; + +use oxc_allocator::CloneIn; +use oxc_ast::ast::{AssignmentOperator, Expression}; +use oxc_span::SPAN; +use oxc_syntax::reference::ReferenceFlags; +use oxc_traverse::BoundIdentifier; + +use crate::context::{TransformCtx, TraverseCtx}; + +impl<'a> TransformCtx<'a> { + /// Duplicate expression to be used twice. + /// + /// If `expr` may have side effects, create a temp var `_expr` and assign to it. + /// + /// * `this` -> `this`, `this` + /// * Bound identifier `foo` -> `foo`, `foo` + /// * Unbound identifier `foo` -> `_foo = foo`, `_foo` + /// * Anything else `foo()` -> `_foo = foo()`, `_foo` + /// + /// If `mutated_symbol_needs_temp_var` is `true`, temp var will be created for a bound identifier, + /// if it's mutated (assigned to) anywhere in AST. + /// + /// Returns 2 `Expression`s. The first may be an `AssignmentExpression`, + /// and must be inserted into output first. + pub(crate) fn duplicate_expression( + &self, + expr: Expression<'a>, + mutated_symbol_needs_temp_var: bool, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, Expression<'a>) { + let (maybe_assignment, references) = + self.duplicate_expression_multiple::<1>(expr, mutated_symbol_needs_temp_var, ctx); + let [reference] = references; + (maybe_assignment, reference) + } + + /// Duplicate expression to be used 3 times. + /// + /// If `expr` may have side effects, create a temp var `_expr` and assign to it. + /// + /// * `this` -> `this`, `this`, `this` + /// * Bound identifier `foo` -> `foo`, `foo`, `foo` + /// * Unbound identifier `foo` -> `_foo = foo`, `_foo`, `_foo` + /// * Anything else `foo()` -> `_foo = foo()`, `_foo`, `_foo` + /// + /// If `mutated_symbol_needs_temp_var` is `true`, temp var will be created for a bound identifier, + /// if it's mutated (assigned to) anywhere in AST. + /// + /// Returns 3 `Expression`s. The first may be an `AssignmentExpression`, + /// and must be inserted into output first. + pub(crate) fn duplicate_expression_twice( + &self, + expr: Expression<'a>, + mutated_symbol_needs_temp_var: bool, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, Expression<'a>, Expression<'a>) { + let (maybe_assignment, references) = + self.duplicate_expression_multiple::<2>(expr, mutated_symbol_needs_temp_var, ctx); + let [reference1, reference2] = references; + (maybe_assignment, reference1, reference2) + } + + /// Duplicate expression `N + 1` times. + /// + /// If `expr` may have side effects, create a temp var `_expr` and assign to it. + /// + /// * `this` -> `this`, [`this`; N] + /// * Bound identifier `foo` -> `foo`, [`foo`; N] + /// * Unbound identifier `foo` -> `_foo = foo`, [`_foo`; N] + /// * Anything else `foo()` -> `_foo = foo()`, [`_foo`; N] + /// + /// If `mutated_symbol_needs_temp_var` is `true`, temp var will be created for a bound identifier, + /// if it's mutated (assigned to) anywhere in AST. + /// + /// Returns `N + 1` x `Expression`s. The first may be an `AssignmentExpression`, + /// and must be inserted into output first. + pub(crate) fn duplicate_expression_multiple( + &self, + expr: Expression<'a>, + mutated_symbol_needs_temp_var: bool, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, [Expression<'a>; N]) { + // TODO: Handle if in a function's params + let temp_var_binding = match &expr { + Expression::Identifier(ident) => { + let reference_id = ident.reference_id(); + let reference = ctx.scoping().get_reference(reference_id); + if let Some(symbol_id) = reference.symbol_id() + && (!mutated_symbol_needs_temp_var + || !ctx.scoping().symbol_is_mutated(symbol_id)) + { + // Reading bound identifier cannot have side effects, so no need for temp var + let binding = BoundIdentifier::new(ident.name, symbol_id); + let references = + create_array(|| binding.create_spanned_read_expression(ident.span, ctx)); + return (expr, references); + } + + // Previously `x += 1` (`x` read + write), but moving to `_x = x` (`x` read only) + let reference = ctx.scoping_mut().get_reference_mut(reference_id); + *reference.flags_mut() = ReferenceFlags::Read; + + self.var_declarations.create_uid_var(&ident.name, ctx) + } + // Reading any of these cannot have side effects, so no need for temp var + Expression::ThisExpression(_) + | Expression::Super(_) + | Expression::BooleanLiteral(_) + | Expression::NullLiteral(_) + | Expression::NumericLiteral(_) + | Expression::BigIntLiteral(_) + | Expression::RegExpLiteral(_) + | Expression::StringLiteral(_) => { + let references = create_array(|| expr.clone_in(ctx.ast.allocator)); + return (expr, references); + } + // Template literal cannot have side effects if it has no expressions. + // If it *does* have expressions, but they're all literals, then also cannot have side effects, + // but don't bother checking for that as it shouldn't occur in real world code. + // Why would you write "`x${9}z`" when you can just write "`x9z`"? + // Note: "`x${foo}`" *can* have side effects if `foo` is an object with a `toString` method. + Expression::TemplateLiteral(lit) if lit.expressions.is_empty() => { + let references = create_array(|| { + ctx.ast.expression_template_literal( + lit.span, + ctx.ast.vec_from_iter(lit.quasis.iter().cloned()), + ctx.ast.vec(), + ) + }); + return (expr, references); + } + // Anything else requires temp var + _ => self.var_declarations.create_uid_var_based_on_node(&expr, ctx), + }; + + let assignment = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + temp_var_binding.create_target(ReferenceFlags::Write, ctx), + expr, + ); + + let references = create_array(|| temp_var_binding.create_read_expression(ctx)); + + (assignment, references) + } +} + +/// Create array of length `N`, with each item initialized with provided function `init`. +/// +/// Implementation based on: +/// * +/// * +// +// `#[inline]` so compiler can inline `init()`, and it may unroll the loop if `init` is simple enough. +#[inline] +fn create_array T>(mut init: I) -> [T; N] { + let mut array: [MaybeUninit; N] = [const { MaybeUninit::uninit() }; N]; + for elem in &mut array { + elem.write(init()); + } + // Wrapping in `ManuallyDrop` should not be necessary because `MaybeUninit` does not impl `Drop`, + // but do it anyway just to make sure, as it's mentioned in issues above. + let mut array = ManuallyDrop::new(array); + // SAFETY: All elements of array are initialized. + // `[MaybeUninit; N]` and `[T; N]` have same layout. + unsafe { ptr::from_mut(&mut array).cast::<[T; N]>().read() } +} diff --git a/crates/swc_ecma_transformer/oxc/common/helper_loader.rs b/crates/swc_ecma_transformer/oxc/common/helper_loader.rs new file mode 100644 index 000000000000..0fee529ce6dc --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/helper_loader.rs @@ -0,0 +1,330 @@ +//! Utility to load helper functions. +//! +//! This module provides functionality to load helper functions in different modes. +//! It supports runtime, external, and inline (not yet implemented) modes for loading helper functions. +//! +//! ## Usage +//! +//! You can call [`TransformCtx::helper_load`] to load a helper function and use it in your CallExpression. +//! +//! ```rs +//! let callee = self.ctx.helper_load("helperName"); +//! let call = self.ctx.ast.call_expression(callee, ...arguments); +//! ``` +//! +//! And also you can call [`TransformCtx::helper_call`] directly to load and call a helper function. +//! +//! ```rs +//! let call_expression = self.ctx.helper_call("helperName", ...arguments); +//! ``` +//! +//! ## Modes +//! +//! ### Runtime ([`HelperLoaderMode::Runtime`]) +//! +//! Uses `@oxc-project/runtime` as a dependency, importing helper functions from the runtime. +//! +//! Generated code example: +//! +//! ```js +//! import helperName from "@oxc-project/runtime/helpers/helperName"; +//! helperName(...arguments); +//! ``` +//! +//! Based on [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/v7.26.2/packages/babel-plugin-transform-runtime). +//! +//! ### External ([`HelperLoaderMode::External`]) +//! +//! Uses helper functions from a global `babelHelpers` variable. This is the default mode for testing. +//! +//! Generated code example: +//! +//! ```js +//! babelHelpers.helperName(...arguments); +//! ``` +//! +//! Based on [@babel/plugin-external-helpers](https://github.com/babel/babel/tree/v7.26.2/packages/babel-plugin-external-helpers). +//! +//! ### Inline ([`HelperLoaderMode::Inline`]) +//! +//! > Note: This mode is not currently implemented. +//! +//! Inline helper functions are inserted directly into the top of program. +//! +//! Generated code example: +//! +//! ```js +//! function helperName(...arguments) { ... } // Inlined helper function +//! helperName(...arguments); +//! ``` +//! +//! Based on [@babel/helper](https://github.com/babel/babel/tree/v7.26.2/packages/babel-helpers). +//! +//! ## Implementation +//! +//! Unlike other "common" utilities, this one has no transformer. It adds imports to the program +//! via `ModuleImports` transform. + +use std::{borrow::Cow, cell::RefCell}; + +use rustc_hash::FxHashMap; +use serde::Deserialize; + +use oxc_allocator::Vec as ArenaVec; +use oxc_ast::{ + NONE, + ast::{Argument, CallExpression, Expression}, +}; +use oxc_semantic::{ReferenceFlags, SymbolFlags}; +use oxc_span::{Atom, SPAN, Span}; +use oxc_traverse::BoundIdentifier; + +use crate::context::{TransformCtx, TraverseCtx}; + +/// Defines the mode for loading helper functions. +#[derive(Default, Clone, Copy, Debug, Deserialize)] +pub enum HelperLoaderMode { + /// Inline mode: Helper functions are directly inserted into the program. + /// + /// Note: This mode is not currently implemented. + /// + /// Example output: + /// ```js + /// function helperName(...arguments) { ... } // Inlined helper function + /// helperName(...arguments); + /// ``` + Inline, + /// External mode: Helper functions are accessed from a global `babelHelpers` object. + /// + /// This is the default mode used in Babel tests. + /// + /// Example output: + /// ```js + /// babelHelpers.helperName(...arguments); + /// ``` + External, + /// Runtime mode: Helper functions are imported from a runtime package. + /// + /// This mode is similar to how @babel/plugin-transform-runtime works. + /// It's the default mode for this implementation. + /// + /// Example output: + /// ```js + /// import helperName from "@oxc-project/runtime/helpers/helperName"; + /// helperName(...arguments); + /// ``` + #[default] + Runtime, +} + +/// Helper loader options. +#[derive(Clone, Debug, Deserialize)] +pub struct HelperLoaderOptions { + #[serde(default = "default_as_module_name")] + /// The module name to import helper functions from. + /// Default: `@oxc-project/runtime` + pub module_name: Cow<'static, str>, + pub mode: HelperLoaderMode, +} + +impl Default for HelperLoaderOptions { + fn default() -> Self { + Self { module_name: default_as_module_name(), mode: HelperLoaderMode::default() } + } +} + +fn default_as_module_name() -> Cow<'static, str> { + Cow::Borrowed("@oxc-project/runtime") +} + +/// Available helpers. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum Helper { + AwaitAsyncGenerator, + AsyncGeneratorDelegate, + AsyncIterator, + AsyncToGenerator, + ObjectSpread2, + WrapAsyncGenerator, + Extends, + ObjectDestructuringEmpty, + ObjectWithoutProperties, + ToPropertyKey, + DefineProperty, + ClassPrivateFieldInitSpec, + ClassPrivateMethodInitSpec, + ClassPrivateFieldGet2, + ClassPrivateFieldSet2, + AssertClassBrand, + ToSetter, + ClassPrivateFieldLooseKey, + ClassPrivateFieldLooseBase, + SuperPropGet, + SuperPropSet, + ReadOnlyError, + WriteOnlyError, + CheckInRHS, + Decorate, + DecorateParam, + DecorateMetadata, + UsingCtx, +} + +impl Helper { + pub const fn name(self) -> &'static str { + match self { + Self::AwaitAsyncGenerator => "awaitAsyncGenerator", + Self::AsyncGeneratorDelegate => "asyncGeneratorDelegate", + Self::AsyncIterator => "asyncIterator", + Self::AsyncToGenerator => "asyncToGenerator", + Self::ObjectSpread2 => "objectSpread2", + Self::WrapAsyncGenerator => "wrapAsyncGenerator", + Self::Extends => "extends", + Self::ObjectDestructuringEmpty => "objectDestructuringEmpty", + Self::ObjectWithoutProperties => "objectWithoutProperties", + Self::ToPropertyKey => "toPropertyKey", + Self::DefineProperty => "defineProperty", + Self::ClassPrivateFieldInitSpec => "classPrivateFieldInitSpec", + Self::ClassPrivateMethodInitSpec => "classPrivateMethodInitSpec", + Self::ClassPrivateFieldGet2 => "classPrivateFieldGet2", + Self::ClassPrivateFieldSet2 => "classPrivateFieldSet2", + Self::AssertClassBrand => "assertClassBrand", + Self::ToSetter => "toSetter", + Self::ClassPrivateFieldLooseKey => "classPrivateFieldLooseKey", + Self::ClassPrivateFieldLooseBase => "classPrivateFieldLooseBase", + Self::SuperPropGet => "superPropGet", + Self::SuperPropSet => "superPropSet", + Self::ReadOnlyError => "readOnlyError", + Self::WriteOnlyError => "writeOnlyError", + Self::CheckInRHS => "checkInRHS", + Self::Decorate => "decorate", + Self::DecorateParam => "decorateParam", + Self::DecorateMetadata => "decorateMetadata", + Self::UsingCtx => "usingCtx", + } + } + + pub const fn pure(self) -> bool { + matches!(self, Self::ClassPrivateFieldLooseKey) + } +} + +/// Stores the state of the helper loader in [`TransformCtx`]. +pub struct HelperLoaderStore<'a> { + module_name: Cow<'static, str>, + mode: HelperLoaderMode, + /// Loaded helpers, determined what helpers are loaded and what imports should be added. + loaded_helpers: RefCell>>, + pub(crate) used_helpers: RefCell>, +} + +impl HelperLoaderStore<'_> { + pub fn new(options: &HelperLoaderOptions) -> Self { + Self { + module_name: options.module_name.clone(), + mode: options.mode, + loaded_helpers: RefCell::new(FxHashMap::default()), + used_helpers: RefCell::new(FxHashMap::default()), + } + } +} + +// Public methods implemented directly on `TransformCtx`, as they need access to `TransformCtx::module_imports`. +impl<'a> TransformCtx<'a> { + /// Load and call a helper function and return a `CallExpression`. + pub fn helper_call( + &self, + helper: Helper, + span: Span, + arguments: ArenaVec<'a, Argument<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> CallExpression<'a> { + let callee = self.helper_load(helper, ctx); + let pure = helper.pure(); + ctx.ast.call_expression_with_pure(span, callee, NONE, arguments, false, pure) + } + + /// Same as [`TransformCtx::helper_call`], but returns a `CallExpression` wrapped in an `Expression`. + pub fn helper_call_expr( + &self, + helper: Helper, + span: Span, + arguments: ArenaVec<'a, Argument<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let callee = self.helper_load(helper, ctx); + let pure = helper.pure(); + ctx.ast.expression_call_with_pure(span, callee, NONE, arguments, false, pure) + } + + /// Load a helper function and return a callee expression. + pub fn helper_load(&self, helper: Helper, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + let helper_loader = &self.helper_loader; + let source = helper_loader.get_runtime_source(helper, ctx); + helper_loader.used_helpers.borrow_mut().entry(helper).or_insert_with(|| source.to_string()); + + match helper_loader.mode { + HelperLoaderMode::Runtime => { + helper_loader.transform_for_runtime_helper(helper, source, self, ctx) + } + HelperLoaderMode::External => { + HelperLoaderStore::transform_for_external_helper(helper, ctx) + } + HelperLoaderMode::Inline => { + unreachable!("Inline helpers are not supported yet"); + } + } + } +} + +// Internal methods +impl<'a> HelperLoaderStore<'a> { + fn transform_for_runtime_helper( + &self, + helper: Helper, + source: Atom<'a>, + transform_ctx: &TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let mut loaded_helpers = self.loaded_helpers.borrow_mut(); + let binding = loaded_helpers + .entry(helper) + .or_insert_with(|| Self::get_runtime_helper(helper, source, transform_ctx, ctx)); + binding.create_read_expression(ctx) + } + + fn get_runtime_helper( + helper: Helper, + source: Atom<'a>, + transform_ctx: &TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + let helper_name = helper.name(); + + let flag = if transform_ctx.source_type.is_module() { + SymbolFlags::Import + } else { + SymbolFlags::FunctionScopedVariable + }; + let binding = ctx.generate_uid_in_root_scope(helper_name, flag); + + transform_ctx.module_imports.add_default_import(source, binding.clone(), false); + + binding + } + + // Construct string directly in arena without an intermediate temp allocation + fn get_runtime_source(&self, helper: Helper, ctx: &TraverseCtx<'a>) -> Atom<'a> { + ctx.ast.atom_from_strs_array([&self.module_name, "/helpers/", helper.name()]) + } + + fn transform_for_external_helper(helper: Helper, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + static HELPER_VAR: &str = "babelHelpers"; + + let symbol_id = ctx.scoping().find_binding(ctx.current_scope_id(), HELPER_VAR); + let object = + ctx.create_ident_expr(SPAN, Atom::from(HELPER_VAR), symbol_id, ReferenceFlags::Read); + let property = ctx.ast.identifier_name(SPAN, Atom::from(helper.name())); + Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)) + } +} diff --git a/crates/swc_ecma_transformer/oxc/common/mod.rs b/crates/swc_ecma_transformer/oxc/common/mod.rs new file mode 100644 index 000000000000..db73269c7577 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/mod.rs @@ -0,0 +1,153 @@ +//! Utility transforms which are in common between other transforms. + +use arrow_function_converter::ArrowFunctionConverter; +use oxc_allocator::Vec as ArenaVec; +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + EnvOptions, + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub mod arrow_function_converter; +mod computed_key; +mod duplicate; +pub mod helper_loader; +pub mod module_imports; +pub mod statement_injector; +pub mod top_level_statements; +pub mod var_declarations; + +use module_imports::ModuleImports; +use statement_injector::StatementInjector; +use top_level_statements::TopLevelStatements; +use var_declarations::VarDeclarations; + +pub struct Common<'a, 'ctx> { + module_imports: ModuleImports<'a, 'ctx>, + var_declarations: VarDeclarations<'a, 'ctx>, + statement_injector: StatementInjector<'a, 'ctx>, + top_level_statements: TopLevelStatements<'a, 'ctx>, + arrow_function_converter: ArrowFunctionConverter<'a>, +} + +impl<'a, 'ctx> Common<'a, 'ctx> { + pub fn new(options: &EnvOptions, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + module_imports: ModuleImports::new(ctx), + var_declarations: VarDeclarations::new(ctx), + statement_injector: StatementInjector::new(ctx), + top_level_statements: TopLevelStatements::new(ctx), + arrow_function_converter: ArrowFunctionConverter::new(options), + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for Common<'a, '_> { + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.module_imports.exit_program(program, ctx); + self.var_declarations.exit_program(program, ctx); + self.top_level_statements.exit_program(program, ctx); + self.arrow_function_converter.exit_program(program, ctx); + self.statement_injector.exit_program(program, ctx); + } + + fn enter_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.var_declarations.enter_statements(stmts, ctx); + } + + fn exit_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.var_declarations.exit_statements(stmts, ctx); + self.statement_injector.exit_statements(stmts, ctx); + } + + fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + self.arrow_function_converter.enter_function(func, ctx); + } + + fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + self.arrow_function_converter.exit_function(func, ctx); + } + + fn enter_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.arrow_function_converter.enter_arrow_function_expression(arrow, ctx); + } + + fn exit_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.arrow_function_converter.exit_arrow_function_expression(arrow, ctx); + } + + fn enter_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) { + self.arrow_function_converter.enter_function_body(body, ctx); + } + + fn exit_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) { + self.arrow_function_converter.exit_function_body(body, ctx); + } + + fn enter_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + self.arrow_function_converter.enter_static_block(block, ctx); + } + + fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + self.arrow_function_converter.exit_static_block(block, ctx); + } + + fn enter_jsx_element_name( + &mut self, + element_name: &mut JSXElementName<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.arrow_function_converter.enter_jsx_element_name(element_name, ctx); + } + + fn enter_jsx_member_expression_object( + &mut self, + object: &mut JSXMemberExpressionObject<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.arrow_function_converter.enter_jsx_member_expression_object(object, ctx); + } + + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.arrow_function_converter.enter_expression(expr, ctx); + } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.arrow_function_converter.exit_expression(expr, ctx); + } + + fn enter_binding_identifier( + &mut self, + node: &mut BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.arrow_function_converter.enter_binding_identifier(node, ctx); + } + + fn enter_identifier_reference( + &mut self, + node: &mut IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.arrow_function_converter.enter_identifier_reference(node, ctx); + } +} diff --git a/crates/swc_ecma_transformer/oxc/common/module_imports.rs b/crates/swc_ecma_transformer/oxc/common/module_imports.rs new file mode 100644 index 000000000000..39042e424e0f --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/module_imports.rs @@ -0,0 +1,250 @@ +//! Utility transform to add `import` / `require` statements to top of program. +//! +//! `ModuleImportsStore` contains an `IndexMap, Vec>>`. +//! It is stored on `TransformCtx`. +//! +//! `ModuleImports` transform +//! +//! Other transforms can add `import`s / `require`s to the store by calling methods of `ModuleImportsStore`: +//! +//! ### Usage +//! +//! ```rs +//! // import { jsx as _jsx } from 'react'; +//! self.ctx.module_imports.add_named_import( +//! Atom::from("react"), +//! Atom::from("jsx"), +//! Atom::from("_jsx"), +//! symbol_id +//! ); +//! +//! // ESM: import React from 'react'; +//! // CJS: var _React = require('react'); +//! self.ctx.module_imports.add_default_import( +//! Atom::from("react"), +//! Atom::from("React"), +//! symbol_id +//! ); +//! ``` +//! +//! > NOTE: Using `import` or `require` is determined by [`TransformCtx::source_type`]. +//! +//! Based on `@babel/helper-module-imports` +//! + +use std::cell::RefCell; + +use indexmap::{IndexMap, map::Entry as IndexMapEntry}; + +use oxc_ast::{NONE, ast::*}; +use oxc_semantic::ReferenceFlags; +use oxc_span::{Atom, SPAN}; +use oxc_syntax::symbol::SymbolId; +use oxc_traverse::{BoundIdentifier, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct ModuleImports<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> ModuleImports<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ModuleImports<'a, '_> { + fn exit_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.ctx.module_imports.insert_into_program(self.ctx, ctx); + } +} + +struct NamedImport<'a> { + imported: Atom<'a>, + local: BoundIdentifier<'a>, +} + +enum Import<'a> { + Named(NamedImport<'a>), + Default(BoundIdentifier<'a>), +} + +/// Store for `import` / `require` statements to be added at top of program. +/// +/// TODO(improve-on-babel): Insertion order does not matter. We only have to use `IndexMap` +/// to produce output that's the same as Babel's. +/// Substitute `FxHashMap` once we don't need to match Babel's output exactly. +pub struct ModuleImportsStore<'a> { + imports: RefCell, Vec>>>, +} + +// Public methods +impl<'a> ModuleImportsStore<'a> { + /// Create new `ModuleImportsStore`. + pub fn new() -> Self { + Self { imports: RefCell::new(IndexMap::default()) } + } + + /// Add default `import` or `require` to top of program. + /// + /// Which it will be depends on the source type. + /// + /// * `import named_import from 'source';` or + /// * `var named_import = require('source');` + /// + /// If `front` is `true`, `import`/`require` is added to front of the `import`s/`require`s. + pub fn add_default_import(&self, source: Atom<'a>, local: BoundIdentifier<'a>, front: bool) { + self.add_import(source, Import::Default(local), front); + } + + /// Add named `import` to top of program. + /// + /// `import { named_import } from 'source';` + /// + /// If `front` is `true`, `import` is added to front of the `import`s. + /// + /// Adding named `require`s is not supported, and will cause a panic later on. + pub fn add_named_import( + &self, + source: Atom<'a>, + imported: Atom<'a>, + local: BoundIdentifier<'a>, + front: bool, + ) { + self.add_import(source, Import::Named(NamedImport { imported, local }), front); + } + + /// Returns `true` if no imports have been scheduled for insertion. + pub fn is_empty(&self) -> bool { + self.imports.borrow().is_empty() + } +} + +// Internal methods +impl<'a> ModuleImportsStore<'a> { + /// Add `import` or `require` to top of program. + /// + /// Which it will be depends on the source type. + /// + /// * `import { named_import } from 'source';` or + /// * `var named_import = require('source');` + /// + /// Adding a named `require` is not supported, and will cause a panic later on. + /// + /// If `front` is `true`, `import`/`require` is added to front of the `import`s/`require`s. + /// TODO(improve-on-babel): `front` option is only required to pass one of Babel's tests. Output + /// without it is still valid. Remove this once our output doesn't need to match Babel exactly. + fn add_import(&self, source: Atom<'a>, import: Import<'a>, front: bool) { + match self.imports.borrow_mut().entry(source) { + IndexMapEntry::Occupied(mut entry) => { + entry.get_mut().push(import); + if front && entry.index() != 0 { + entry.move_index(0); + } + } + IndexMapEntry::Vacant(entry) => { + let named_imports = vec![import]; + if front { + entry.shift_insert(0, named_imports); + } else { + entry.insert(named_imports); + } + } + } + } + + /// Insert `import` / `require` statements at top of program. + fn insert_into_program(&self, transform_ctx: &TransformCtx<'a>, ctx: &mut TraverseCtx<'a>) { + if transform_ctx.source_type.is_script() { + self.insert_require_statements(transform_ctx, ctx); + } else { + self.insert_import_statements(transform_ctx, ctx); + } + } + + fn insert_import_statements(&self, transform_ctx: &TransformCtx<'a>, ctx: &TraverseCtx<'a>) { + let mut imports = self.imports.borrow_mut(); + let stmts = imports.drain(..).map(|(source, names)| Self::get_import(source, names, ctx)); + transform_ctx.top_level_statements.insert_statements(stmts); + } + + fn insert_require_statements( + &self, + transform_ctx: &TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let mut imports = self.imports.borrow_mut(); + if imports.is_empty() { + return; + } + + let require_symbol_id = ctx.scoping().get_root_binding("require"); + let stmts = imports + .drain(..) + .map(|(source, names)| Self::get_require(source, names, require_symbol_id, ctx)); + transform_ctx.top_level_statements.insert_statements(stmts); + } + + fn get_import( + source: Atom<'a>, + names: Vec>, + ctx: &TraverseCtx<'a>, + ) -> Statement<'a> { + let specifiers = ctx.ast.vec_from_iter(names.into_iter().map(|import| match import { + Import::Named(import) => { + ImportDeclarationSpecifier::ImportSpecifier(ctx.ast.alloc_import_specifier( + SPAN, + ModuleExportName::IdentifierName( + ctx.ast.identifier_name(SPAN, import.imported), + ), + import.local.create_binding_identifier(ctx), + ImportOrExportKind::Value, + )) + } + Import::Default(local) => ImportDeclarationSpecifier::ImportDefaultSpecifier( + ctx.ast.alloc_import_default_specifier(SPAN, local.create_binding_identifier(ctx)), + ), + })); + + Statement::from(ctx.ast.module_declaration_import_declaration( + SPAN, + Some(specifiers), + ctx.ast.string_literal(SPAN, source, None), + None, + NONE, + ImportOrExportKind::Value, + )) + } + + fn get_require( + source: Atom<'a>, + names: std::vec::Vec>, + require_symbol_id: Option, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let callee = ctx.create_ident_expr( + SPAN, + Atom::from("require"), + require_symbol_id, + ReferenceFlags::read(), + ); + + let args = { + let arg = Argument::from(ctx.ast.expression_string_literal(SPAN, source, None)); + ctx.ast.vec1(arg) + }; + let Some(Import::Default(local)) = names.into_iter().next() else { unreachable!() }; + let id = local.create_binding_pattern(ctx); + let var_kind = VariableDeclarationKind::Var; + let decl = { + let init = ctx.ast.expression_call(SPAN, callee, NONE, args, false); + let decl = ctx.ast.variable_declarator(SPAN, var_kind, id, Some(init), false); + ctx.ast.vec1(decl) + }; + Statement::from(ctx.ast.declaration_variable(SPAN, var_kind, decl, false)) + } +} diff --git a/crates/swc_ecma_transformer/oxc/common/statement_injector.rs b/crates/swc_ecma_transformer/oxc/common/statement_injector.rs new file mode 100644 index 000000000000..3145e47c2bda --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/statement_injector.rs @@ -0,0 +1,244 @@ +//! Utility transform to add new statements before or after the specified statement. +//! +//! `StatementInjectorStore` contains a `FxHashMap>`. It is stored on `TransformCtx`. +//! +//! `StatementInjector` transform inserts new statements before or after a statement which is determined by the address of the statement. +//! +//! Other transforms can add statements to the store with following methods: +//! +//! ```rs +//! self.ctx.statement_injector.insert_before(address, statement); +//! self.ctx.statement_injector.insert_after(address, statement); +//! self.ctx.statement_injector.insert_many_after(address, statements); +//! ``` + +use std::{cell::RefCell, collections::hash_map::Entry}; + +use rustc_hash::FxHashMap; + +use oxc_allocator::{Address, GetAddress, Vec as ArenaVec}; +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +/// Transform that inserts any statements which have been requested insertion via `StatementInjectorStore` +pub struct StatementInjector<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> StatementInjector<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for StatementInjector<'a, '_> { + fn exit_statements( + &mut self, + statements: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.ctx.statement_injector.insert_into_statements(statements, ctx); + } + + #[inline] + fn exit_program(&mut self, _program: &mut Program<'a>, _ctx: &mut TraverseCtx<'a>) { + self.ctx.statement_injector.assert_no_insertions_remaining(); + } +} + +#[derive(Debug)] +enum Direction { + Before, + After, +} + +#[derive(Debug)] +struct AdjacentStatement<'a> { + stmt: Statement<'a>, + direction: Direction, +} + +/// Store for statements to be added to the statements. +pub struct StatementInjectorStore<'a> { + insertions: RefCell>>>, +} + +// Public methods +impl StatementInjectorStore<'_> { + /// Create new `StatementInjectorStore`. + pub fn new() -> Self { + Self { insertions: RefCell::new(FxHashMap::default()) } + } +} + +// Insertion methods. +// +// Each of these functions is split into 2 parts: +// +// 1. Public outer function which is generic over any `GetAddress`. +// 2. Private inner function which is non-generic and takes `Address`. +// +// Outer functions are marked `#[inline]`, as `GetAddress::address` is generally only 1 or 2 instructions. +// The non-trivial inner functions are not marked `#[inline]` - compiler can decide whether to inline or not. +impl<'a> StatementInjectorStore<'a> { + /// Add a statement to be inserted immediately before the target statement. + #[inline] + pub fn insert_before(&self, target: &A, stmt: Statement<'a>) { + self.insert_before_address(target.address(), stmt); + } + + fn insert_before_address(&self, target: Address, stmt: Statement<'a>) { + let mut insertions = self.insertions.borrow_mut(); + let adjacent_stmts = insertions.entry(target).or_default(); + let index = adjacent_stmts + .iter() + .position(|s| matches!(s.direction, Direction::After)) + .unwrap_or(adjacent_stmts.len()); + adjacent_stmts.insert(index, AdjacentStatement { stmt, direction: Direction::Before }); + } + + /// Add a statement to be inserted immediately after the target statement. + #[inline] + pub fn insert_after(&self, target: &A, stmt: Statement<'a>) { + self.insert_after_address(target.address(), stmt); + } + + fn insert_after_address(&self, target: Address, stmt: Statement<'a>) { + let mut insertions = self.insertions.borrow_mut(); + let adjacent_stmts = insertions.entry(target).or_default(); + adjacent_stmts.push(AdjacentStatement { stmt, direction: Direction::After }); + } + + /// Add multiple statements to be inserted immediately before the target statement. + #[inline] + pub fn insert_many_before(&self, target: &A, stmts: S) + where + A: GetAddress, + S: IntoIterator>, + { + self.insert_many_before_address(target.address(), stmts); + } + + fn insert_many_before_address(&self, target: Address, stmts: S) + where + S: IntoIterator>, + { + let mut insertions = self.insertions.borrow_mut(); + let adjacent_stmts = insertions.entry(target).or_default(); + adjacent_stmts.splice( + 0..0, + stmts.into_iter().map(|stmt| AdjacentStatement { stmt, direction: Direction::Before }), + ); + } + + /// Add multiple statements to be inserted immediately after the target statement. + #[inline] + pub fn insert_many_after(&self, target: &A, stmts: S) + where + A: GetAddress, + S: IntoIterator>, + { + self.insert_many_after_address(target.address(), stmts); + } + + fn insert_many_after_address(&self, target: Address, stmts: S) + where + S: IntoIterator>, + { + let mut insertions = self.insertions.borrow_mut(); + let adjacent_stmts = insertions.entry(target).or_default(); + adjacent_stmts.extend( + stmts.into_iter().map(|stmt| AdjacentStatement { stmt, direction: Direction::After }), + ); + } + + /// Move insertions from one [`Address`] to another. + /// + /// Use this if you convert one statement to another, and other code may have attached + /// insertions to the original statement. + #[inline] + pub fn move_insertions( + &self, + old_target: &A1, + new_target: &A2, + ) { + self.move_insertions_address(old_target.address(), new_target.address()); + } + + fn move_insertions_address(&self, old_address: Address, new_address: Address) { + let mut insertions = self.insertions.borrow_mut(); + let Some(mut adjacent_stmts) = insertions.remove(&old_address) else { return }; + + match insertions.entry(new_address) { + Entry::Occupied(entry) => { + entry.into_mut().append(&mut adjacent_stmts); + } + Entry::Vacant(entry) => { + entry.insert(adjacent_stmts); + } + } + } +} + +// Internal methods +impl<'a> StatementInjectorStore<'a> { + /// Insert statements immediately before / after the target statement. + fn insert_into_statements( + &self, + statements: &mut ArenaVec<'a, Statement<'a>>, + ctx: &TraverseCtx<'a>, + ) { + let mut insertions = self.insertions.borrow_mut(); + if insertions.is_empty() { + return; + } + + let new_statement_count = statements + .iter() + .filter_map(|s| insertions.get(&s.address()).map(Vec::len)) + .sum::(); + if new_statement_count == 0 { + return; + } + + let mut new_statements = ctx.ast.vec_with_capacity(statements.len() + new_statement_count); + + for stmt in statements.drain(..) { + match insertions.remove(&stmt.address()) { + Some(mut adjacent_stmts) => { + let first_after_stmt_index = adjacent_stmts + .iter() + .position(|s| matches!(s.direction, Direction::After)) + .unwrap_or(adjacent_stmts.len()); + if first_after_stmt_index != 0 { + let right = adjacent_stmts.split_off(first_after_stmt_index); + new_statements.extend(adjacent_stmts.into_iter().map(|s| s.stmt)); + new_statements.push(stmt); + new_statements.extend(right.into_iter().map(|s| s.stmt)); + } else { + new_statements.push(stmt); + new_statements.extend(adjacent_stmts.into_iter().map(|s| s.stmt)); + } + } + _ => { + new_statements.push(stmt); + } + } + } + + *statements = new_statements; + } + + // Assertion for checking if no remaining insertions are left. + // `#[inline(always)]` because this is a no-op in release mode + #[expect(clippy::inline_always)] + #[inline(always)] + fn assert_no_insertions_remaining(&self) { + debug_assert!(self.insertions.borrow().is_empty()); + } +} diff --git a/crates/swc_ecma_transformer/oxc/common/top_level_statements.rs b/crates/swc_ecma_transformer/oxc/common/top_level_statements.rs new file mode 100644 index 000000000000..6240deaac4a5 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/top_level_statements.rs @@ -0,0 +1,86 @@ +//! Utility transform to add statements to top of program. +//! +//! `TopLevelStatementsStore` contains a `Vec`. It is stored on `TransformCtx`. +//! +//! `TopLevelStatements` transform inserts those statements at top of program. +//! +//! Other transforms can add statements to the store with `TopLevelStatementsStore::insert_statement`: +//! +//! ```rs +//! self.ctx.top_level_statements.insert_statement(stmt); +//! ``` + +use std::cell::RefCell; + +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +/// Transform that inserts any statements which have been requested insertion via `TopLevelStatementsStore` +/// to top of the program. +/// +/// Insertions are made after any existing `import` statements. +/// +/// Must run after all other transforms. +pub struct TopLevelStatements<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> TopLevelStatements<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for TopLevelStatements<'a, '_> { + fn exit_program(&mut self, program: &mut Program<'a>, _ctx: &mut TraverseCtx<'a>) { + self.ctx.top_level_statements.insert_into_program(program); + } +} + +/// Store for statements to be added at top of program +pub struct TopLevelStatementsStore<'a> { + stmts: RefCell>>, +} + +// Public methods +impl<'a> TopLevelStatementsStore<'a> { + /// Create new `TopLevelStatementsStore`. + pub fn new() -> Self { + Self { stmts: RefCell::new(vec![]) } + } + + /// Add a statement to be inserted at top of program. + pub fn insert_statement(&self, stmt: Statement<'a>) { + self.stmts.borrow_mut().push(stmt); + } + + /// Add statements to be inserted at top of program. + pub fn insert_statements>>(&self, stmts: I) { + self.stmts.borrow_mut().extend(stmts); + } +} + +// Internal methods +impl<'a> TopLevelStatementsStore<'a> { + /// Insert statements at top of program. + fn insert_into_program(&self, program: &mut Program<'a>) { + let mut stmts = self.stmts.borrow_mut(); + if stmts.is_empty() { + return; + } + + // Insert statements before the first non-import statement. + let index = program + .body + .iter() + .position(|stmt| !matches!(stmt, Statement::ImportDeclaration(_))) + .unwrap_or(program.body.len()); + + program.body.splice(index..index, stmts.drain(..)); + } +} diff --git a/crates/swc_ecma_transformer/oxc/common/var_declarations.rs b/crates/swc_ecma_transformer/oxc/common/var_declarations.rs new file mode 100644 index 000000000000..7aee7cbace68 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/common/var_declarations.rs @@ -0,0 +1,278 @@ +//! Utility transform to add `var` or `let` declarations to top of statement blocks. +//! +//! `VarDeclarationsStore` contains a stack of `Declarators`s, each comprising +//! 2 x `Vec>` (1 for `var`s, 1 for `let`s). +//! `VarDeclarationsStore` is stored on `TransformCtx`. +//! +//! `VarDeclarations` transform pushes an empty entry onto this stack when entering a statement block, +//! and when exiting the block, writes `var` / `let` statements to top of block. +//! +//! Other transforms can add declarators to the store by calling methods of `VarDeclarationsStore`: +//! +//! ```rs +//! self.ctx.var_declarations.insert_var(name, binding, None, ctx); +//! self.ctx.var_declarations.insert_let(name2, binding2, None, ctx); +//! ``` + +use std::cell::RefCell; + +use oxc_allocator::Vec as ArenaVec; +use oxc_ast::ast::*; +use oxc_data_structures::stack::SparseStack; +use oxc_span::SPAN; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, ast_operations::GatherNodeParts}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +/// Transform that maintains the stack of `Vec`s, and adds a `var` statement +/// to top of a statement block if another transform has requested that. +/// +/// Must run after all other transforms except `TopLevelStatements`. +pub struct VarDeclarations<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> VarDeclarations<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for VarDeclarations<'a, '_> { + fn enter_statements( + &mut self, + _stmts: &mut ArenaVec<'a, Statement<'a>>, + _ctx: &mut TraverseCtx<'a>, + ) { + self.ctx.var_declarations.record_entering_statements(); + } + + fn exit_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.ctx.var_declarations.insert_into_statements(stmts, ctx); + } + + fn exit_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.ctx.var_declarations.insert_into_program(self.ctx, ctx); + } +} + +/// Store for `VariableDeclarator`s to be added to enclosing statement block. +pub struct VarDeclarationsStore<'a> { + stack: RefCell>>, +} + +/// Declarators to be inserted in a statement block. +struct Declarators<'a> { + var_declarators: ArenaVec<'a, VariableDeclarator<'a>>, + let_declarators: ArenaVec<'a, VariableDeclarator<'a>>, +} + +impl<'a> Declarators<'a> { + fn new(ctx: &TraverseCtx<'a>) -> Self { + Self { var_declarators: ctx.ast.vec(), let_declarators: ctx.ast.vec() } + } +} + +// Public methods +impl<'a> VarDeclarationsStore<'a> { + /// Create new `VarDeclarationsStore`. + pub fn new() -> Self { + Self { stack: RefCell::new(SparseStack::new()) } + } + + /// Add a `var` declaration to be inserted at top of current enclosing statement block, + /// given a `BoundIdentifier`. + #[inline] + pub fn insert_var(&self, binding: &BoundIdentifier<'a>, ctx: &TraverseCtx<'a>) { + let pattern = binding.create_binding_pattern(ctx); + self.insert_var_binding_pattern(pattern, None, ctx); + } + + /// Add a `var` declaration with the given init expression to be inserted at top of + /// current enclosing statement block, given a `BoundIdentifier`. + #[inline] + pub fn insert_var_with_init( + &self, + binding: &BoundIdentifier<'a>, + init: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) { + let pattern = binding.create_binding_pattern(ctx); + self.insert_var_binding_pattern(pattern, Some(init), ctx); + } + + /// Create a new UID based on `name`, add a `var` declaration to be inserted at the top of + /// the current enclosing statement block, and return the [`BoundIdentifier`]. + #[inline] + pub fn create_uid_var(&self, name: &str, ctx: &mut TraverseCtx<'a>) -> BoundIdentifier<'a> { + let binding = ctx.generate_uid_in_current_hoist_scope(name); + self.insert_var(&binding, ctx); + binding + } + + /// Create a new UID based on `name`, add a `var` declaration with the given init expression + /// to be inserted at the top of the current enclosing statement block, and return the + /// [`BoundIdentifier`]. + #[inline] + pub fn create_uid_var_with_init( + &self, + name: &str, + expression: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + let binding = ctx.generate_uid_in_current_hoist_scope(name); + self.insert_var_with_init(&binding, expression, ctx); + binding + } + + /// Create a new UID with name based on `node`, add a `var` declaration to be inserted + /// at the top of the current enclosing statement block, and return the [`BoundIdentifier`]. + #[inline] + pub fn create_uid_var_based_on_node>( + &self, + node: &N, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + let binding = ctx.generate_uid_in_current_hoist_scope_based_on_node(node); + self.insert_var(&binding, ctx); + binding + } + + /// Add a `let` declaration to be inserted at top of current enclosing statement block, + /// given a `BoundIdentifier`. + pub fn insert_let( + &self, + binding: &BoundIdentifier<'a>, + init: Option>, + ctx: &TraverseCtx<'a>, + ) { + let pattern = binding.create_binding_pattern(ctx); + self.insert_let_binding_pattern(pattern, init, ctx); + } + + /// Add a `var` declaration to be inserted at top of current enclosing statement block, + /// given a `BindingPattern`. + pub fn insert_var_binding_pattern( + &self, + ident: BindingPattern<'a>, + init: Option>, + ctx: &TraverseCtx<'a>, + ) { + let declarator = + ctx.ast.variable_declarator(SPAN, VariableDeclarationKind::Var, ident, init, false); + self.insert_var_declarator(declarator, ctx); + } + + /// Add a `let` declaration to be inserted at top of current enclosing statement block, + /// given a `BindingPattern`. + pub fn insert_let_binding_pattern( + &self, + ident: BindingPattern<'a>, + init: Option>, + ctx: &TraverseCtx<'a>, + ) { + let declarator = + ctx.ast.variable_declarator(SPAN, VariableDeclarationKind::Let, ident, init, false); + self.insert_let_declarator(declarator, ctx); + } + + /// Add a `var` declaration to be inserted at top of current enclosing statement block. + pub fn insert_var_declarator(&self, declarator: VariableDeclarator<'a>, ctx: &TraverseCtx<'a>) { + let mut stack = self.stack.borrow_mut(); + let declarators = stack.last_mut_or_init(|| Declarators::new(ctx)); + declarators.var_declarators.push(declarator); + } + + /// Add a `let` declaration to be inserted at top of current enclosing statement block. + pub fn insert_let_declarator(&self, declarator: VariableDeclarator<'a>, ctx: &TraverseCtx<'a>) { + let mut stack = self.stack.borrow_mut(); + let declarators = stack.last_mut_or_init(|| Declarators::new(ctx)); + declarators.let_declarators.push(declarator); + } +} + +// Internal methods +impl<'a> VarDeclarationsStore<'a> { + fn record_entering_statements(&self) { + let mut stack = self.stack.borrow_mut(); + stack.push(None); + } + + fn insert_into_statements( + &self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &TraverseCtx<'a>, + ) { + if matches!(ctx.parent(), Ancestor::ProgramBody(_)) { + // Handle in `insert_into_program` instead + return; + } + + if let Some((var_statement, let_statement)) = self.get_var_statement(ctx) { + let mut new_stmts = ctx.ast.vec_with_capacity(stmts.len() + 2); + match (var_statement, let_statement) { + (Some(var_statement), Some(let_statement)) => { + // Insert `var` and `let` statements + new_stmts.extend([var_statement, let_statement]); + } + (Some(statement), None) | (None, Some(statement)) => { + // Insert `var` or `let` statement + new_stmts.push(statement); + } + (None, None) => return, + } + new_stmts.append(stmts); + *stmts = new_stmts; + } + } + + fn insert_into_program(&self, transform_ctx: &TransformCtx<'a>, ctx: &TraverseCtx<'a>) { + if let Some((var_statement, let_statement)) = self.get_var_statement(ctx) { + // Delegate to `TopLevelStatements` + transform_ctx + .top_level_statements + .insert_statements(var_statement.into_iter().chain(let_statement)); + } + + // Check stack is exhausted + let stack = self.stack.borrow(); + debug_assert!(stack.is_exhausted()); + debug_assert!(stack.last().is_none()); + } + + #[inline] + fn get_var_statement( + &self, + ctx: &TraverseCtx<'a>, + ) -> Option<(Option>, Option>)> { + let mut stack = self.stack.borrow_mut(); + let Declarators { var_declarators, let_declarators } = stack.pop()?; + + let var_statement = (!var_declarators.is_empty()) + .then(|| Self::create_declaration(VariableDeclarationKind::Var, var_declarators, ctx)); + let let_statement = (!let_declarators.is_empty()) + .then(|| Self::create_declaration(VariableDeclarationKind::Let, let_declarators, ctx)); + + Some((var_statement, let_statement)) + } + + fn create_declaration( + kind: VariableDeclarationKind, + declarators: ArenaVec<'a, VariableDeclarator<'a>>, + ctx: &TraverseCtx<'a>, + ) -> Statement<'a> { + Statement::VariableDeclaration(ctx.ast.alloc_variable_declaration( + SPAN, + kind, + declarators, + false, + )) + } +} diff --git a/crates/swc_ecma_transformer/oxc/compiler_assumptions.rs b/crates/swc_ecma_transformer/oxc/compiler_assumptions.rs new file mode 100644 index 000000000000..2e084abf86f8 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/compiler_assumptions.rs @@ -0,0 +1,135 @@ +use serde::Deserialize; + +/// Compiler assumptions +/// +/// For producing smaller output. +/// +/// See +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CompilerAssumptions { + #[serde(default)] + #[deprecated = "Not Implemented"] + pub array_like_is_iterable: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub constant_reexports: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub constant_super: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub enumerable_module_meta: bool, + + #[serde(default)] + pub ignore_function_length: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub ignore_to_primitive_hint: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub iterable_is_array: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub mutable_template_object: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub no_class_calls: bool, + + #[serde(default)] + pub no_document_all: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub no_incomplete_ns_import_detection: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub no_new_arrows: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub no_uninitialized_private_field_access: bool, + + #[serde(default)] + pub object_rest_no_symbols: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub private_fields_as_symbols: bool, + + #[serde(default)] + pub private_fields_as_properties: bool, + + #[serde(default)] + pub pure_getters: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub set_class_methods: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub set_computed_properties: bool, + + /// When using public class fields, assume that they don't shadow any getter in the current class, + /// in its subclasses or in its superclass. Thus, it's safe to assign them rather than using + /// `Object.defineProperty`. + /// + /// For example: + /// + /// Input: + /// ```js + /// class Test { + /// field = 2; + /// + /// static staticField = 3; + /// } + /// ``` + /// + /// When `set_public_class_fields` is `true`, the output will be: + /// ```js + /// class Test { + /// constructor() { + /// this.field = 2; + /// } + /// } + /// Test.staticField = 3; + /// ``` + /// + /// Otherwise, the output will be: + /// ```js + /// import _defineProperty from "@oxc-project/runtime/helpers/defineProperty"; + /// class Test { + /// constructor() { + /// _defineProperty(this, "field", 2); + /// } + /// } + /// _defineProperty(Test, "staticField", 3); + /// ``` + /// + /// NOTE: For TypeScript, if you wanted behavior is equivalent to `useDefineForClassFields: false`, you should + /// set both `set_public_class_fields` and [`crate::TypeScriptOptions::remove_class_fields_without_initializer`] + /// to `true`. + #[serde(default)] + pub set_public_class_fields: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub set_spread_properties: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub skip_for_of_iterator_closing: bool, + + #[serde(default)] + #[deprecated = "Not Implemented"] + pub super_is_callable_constructor: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/context.rs b/crates/swc_ecma_transformer/oxc/context.rs new file mode 100644 index 000000000000..8472ffffb8a6 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/context.rs @@ -0,0 +1,87 @@ +use std::{ + cell::RefCell, + mem, + path::{Path, PathBuf}, +}; + +use oxc_diagnostics::OxcDiagnostic; +use oxc_span::SourceType; + +use crate::{ + CompilerAssumptions, Module, TransformOptions, + common::{ + helper_loader::HelperLoaderStore, module_imports::ModuleImportsStore, + statement_injector::StatementInjectorStore, top_level_statements::TopLevelStatementsStore, + var_declarations::VarDeclarationsStore, + }, + state::TransformState, +}; + +pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, TransformState<'a>>; + +pub struct TransformCtx<'a> { + errors: RefCell>, + + /// + pub filename: String, + + /// Source path in the form of `/path/to/file/input.js` + pub source_path: PathBuf, + + pub source_type: SourceType, + + pub source_text: &'a str, + + pub module: Module, + + pub assumptions: CompilerAssumptions, + + // Helpers + /// Manage helper loading + pub helper_loader: HelperLoaderStore<'a>, + /// Manage import statement globally + pub module_imports: ModuleImportsStore<'a>, + /// Manage inserting `var` statements globally + pub var_declarations: VarDeclarationsStore<'a>, + /// Manage inserting statements immediately before or after the target statement + pub statement_injector: StatementInjectorStore<'a>, + /// Manage inserting statements at top of program globally + pub top_level_statements: TopLevelStatementsStore<'a>, + + // State for multiple plugins interacting + /// `true` if class properties plugin is enabled + pub is_class_properties_plugin_enabled: bool, +} + +impl TransformCtx<'_> { + pub fn new(source_path: &Path, options: &TransformOptions) -> Self { + let filename = source_path + .file_stem() // omit file extension + .map_or_else(|| String::from("unknown"), |name| name.to_string_lossy().to_string()); + + Self { + errors: RefCell::new(vec![]), + filename, + source_path: source_path.to_path_buf(), + source_type: SourceType::default(), + source_text: "", + module: options.env.module, + assumptions: options.assumptions, + helper_loader: HelperLoaderStore::new(&options.helper_loader), + module_imports: ModuleImportsStore::new(), + var_declarations: VarDeclarationsStore::new(), + statement_injector: StatementInjectorStore::new(), + top_level_statements: TopLevelStatementsStore::new(), + is_class_properties_plugin_enabled: options.env.es2022.class_properties.is_some(), + } + } + + pub fn take_errors(&self) -> Vec { + mem::take(&mut self.errors.borrow_mut()) + } + + /// Add an Error + pub fn error(&self, error: OxcDiagnostic) { + self.errors.borrow_mut().push(error); + } +} diff --git a/crates/swc_ecma_transformer/oxc/decorator/legacy/metadata.rs b/crates/swc_ecma_transformer/oxc/decorator/legacy/metadata.rs new file mode 100644 index 000000000000..dfbeb17d60c9 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/decorator/legacy/metadata.rs @@ -0,0 +1,803 @@ +/// Emitting decorator metadata. +/// +/// This plugin is used to emit decorator metadata for legacy decorators by +/// the `__metadata` helper. +/// +/// ## Example +/// +/// Input: +/// ```ts +/// class Demo { +/// @LogMethod +/// public foo(bar: number) {} +/// +/// @Prop +/// prop: string = "hello"; +/// } +/// ``` +/// +/// Output: +/// ```js +/// class Demo { +/// foo(bar) {} +/// prop = "hello"; +/// } +/// babelHelpers.decorate([ +/// LogMethod, +/// babelHelpers.decorateParam(0, babelHelpers.decorateMetadata("design:type", Function)), +/// babelHelpers.decorateParam(0, babelHelpers.decorateMetadata("design:paramtypes", [Number])), +/// babelHelpers.decorateParam(0, babelHelpers.decorateMetadata("design:returntype", void 0)) +/// ], Demo.prototype, "foo", null); +/// babelHelpers.decorate([Prop, babelHelpers.decorateMetadata("design:type", String)], Demo.prototype, "prop", void 0); +/// ``` +/// +/// ## Implementation +/// +/// Implementation based on https://github.com/microsoft/TypeScript/blob/d85767abfd83880cea17cea70f9913e9c4496dcc/src/compiler/transformers/ts.ts#L1119-L1136 +/// +/// ## Limitations +/// +/// ### Compared to TypeScript +/// +/// We are lacking a kind of type inference ability that TypeScript has, so we are not able to determine +/// the exactly type of the type reference. See [`LegacyDecoratorMetadata::serialize_type_reference_node`] does. +/// +/// For example: +/// +/// Input: +/// ```ts +/// type Foo = string; +/// class Cls { +/// @dec +/// p: Foo = "" +/// } +/// ``` +/// +/// TypeScript Output: +/// ```js +/// class Cls { +/// constructor() { +/// this.p = ""; +/// } +/// } +/// __decorate([ +/// dec, +/// __metadata("design:type", String) // Infer the type of `Foo` is `String` +/// ], Cls.prototype, "p", void 0); +/// ``` +/// +/// OXC Output: +/// ```js +/// var _ref; +/// class Cls { +/// p = ""; +/// } +/// babelHelpers.decorate([ +/// dec, +/// babelHelpers.decorateMetadata("design:type", typeof (_ref = typeof Foo === "undefined" && Foo) === "function" ? _ref : Object) +/// ], +/// Cls.prototype, "p", void 0); +/// ``` +/// +/// ### Compared to SWC +/// +/// SWC also has the above limitation, considering that SWC has been adopted in [NestJS](https://docs.nestjs.com/recipes/swc#jest--swc), +/// so the limitation may not be a problem. In addition, SWC provides additional support for inferring enum members, which we currently +/// do not have. We haven't dived into how NestJS uses it, so we don't know if it matters, thus we may leave it until we receive feedback. +/// +/// ## References +/// * TypeScript's [emitDecoratorMetadata](https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata) +use oxc_allocator::{Box as ArenaBox, TakeIn}; +use oxc_ast::ast::*; +use oxc_data_structures::stack::SparseStack; +use oxc_semantic::{Reference, ReferenceFlags, SymbolId}; +use oxc_span::{ContentEq, SPAN}; +use oxc_traverse::{MaybeBoundIdentifier, Traverse}; +use rustc_hash::FxHashMap; + +use crate::{ + Helper, + context::{TransformCtx, TraverseCtx}, + state::TransformState, + utils::ast_builder::create_property_access, +}; + +/// Type of an enum inferred from its members +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EnumType { + /// All members are string literals or template literals with string-only expressions + String, + /// All members are numeric, bigint, unary numeric, or auto-incremented + Number, + /// Mixed types or computed values + Object, +} + +/// Metadata for decorated methods +pub(super) struct MethodMetadata<'a> { + /// The `design:type` metadata expression + pub r#type: Expression<'a>, + /// The `design:paramtypes` metadata expression + pub param_types: Expression<'a>, + /// The `design:returntype` metadata expression (optional, omitted for getters/setters) + pub return_type: Option>, +} + +pub struct LegacyDecoratorMetadata<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + /// Stack of method metadata. + /// + /// Only the method that needs to be pushed onto a stack is the method metadata, + /// which should be inserted after all real decorators. However, method parameters + /// will be processed before the metadata generation, so we need to temporarily store + /// them in a stack and pop them when in exit_method_definition. + method_metadata_stack: SparseStack>, + /// Stack of constructor metadata expressions, each expression + /// is the `design:paramtypes`. + /// + /// Same as `method_metadata_stack`, but for constructors. Because the constructor is specially treated + /// in the class, we need to handle it in `exit_class` rather than `exit_method_definition`. + constructor_metadata_stack: SparseStack>, + enum_types: FxHashMap, +} + +impl<'a, 'ctx> LegacyDecoratorMetadata<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + LegacyDecoratorMetadata { + ctx, + method_metadata_stack: SparseStack::new(), + constructor_metadata_stack: SparseStack::new(), + enum_types: FxHashMap::default(), + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for LegacyDecoratorMetadata<'a, '_> { + #[inline] + fn exit_program(&mut self, _program: &mut Program<'a>, _ctx: &mut TraverseCtx<'a>) { + debug_assert!( + self.method_metadata_stack.is_exhausted(), + "All method metadata should have been popped." + ); + debug_assert!( + self.constructor_metadata_stack.is_exhausted(), + "All constructor metadata should have been popped." + ); + } + + // `#[inline]` because this is a hot path and most `Statement`s are not `TSEnumDeclaration`s. + // We want to avoid overhead of a function call for the common case. + #[inline] + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + // Collect enum types here instead of in `enter_ts_enum_declaration` because the TypeScript + // plugin transforms enum declarations in `enter_statement`, and we need to collect the + // enum type before it gets transformed. + if let Statement::TSEnumDeclaration(decl) = stmt { + self.collect_enum_type(decl, ctx); + } + } + + fn enter_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + let should_transform = !(class.is_expression() || class.declare); + + let constructor = class.body.body.iter_mut().find_map(|item| match item { + ClassElement::MethodDefinition(method) if method.kind.is_constructor() => Some(method), + _ => None, + }); + + let metadata = if should_transform + && let Some(constructor) = constructor + && !(class.decorators.is_empty() + && constructor.value.params.items.iter().all(|param| param.decorators.is_empty())) + { + let serialized_type = + self.serialize_parameters_types_of_node(&constructor.value.params, ctx); + + Some(self.create_metadata("design:paramtypes", serialized_type, ctx)) + } else { + None + }; + + self.constructor_metadata_stack.push(metadata); + } + + fn enter_method_definition( + &mut self, + method: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if method.kind.is_constructor() { + // Handle constructor in `enter_class` + return; + } + + let is_typescript_syntax = method.value.is_typescript_syntax(); + let is_decorated = !is_typescript_syntax + && (!method.decorators.is_empty() + || method.value.params.items.iter().any(|param| !param.decorators.is_empty())); + + let metadata = is_decorated.then(|| { + // TypeScript only emits `design:returntype` for regular methods, + // not for getters or setters. + + let (design_type, return_type) = if method.kind.is_get() { + // For getters, the design type is the type of the property + (self.serialize_return_type_of_node(&method.value, ctx), None) + } else if method.kind.is_set() + && let Some(param) = method.value.params.items.first() + { + // For setters, the design type is the type of the first parameter + (self.serialize_parameter_types_of_node(param, ctx), None) + } else { + // For methods, the design type is always `Function` + ( + Self::global_function(ctx), + Some(self.serialize_return_type_of_node(&method.value, ctx)), + ) + }; + + let param_types = self.serialize_parameters_types_of_node(&method.value.params, ctx); + + MethodMetadata { + r#type: self.create_metadata("design:type", design_type, ctx), + param_types: self.create_metadata("design:paramtypes", param_types, ctx), + return_type: return_type.map(|t| self.create_metadata("design:returntype", t, ctx)), + } + }); + + self.method_metadata_stack.push(metadata); + } + + #[inline] + fn enter_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if prop.decorators.is_empty() { + return; + } + prop.decorators.push(self.create_design_type_metadata(prop.type_annotation.as_ref(), ctx)); + } + + #[inline] + fn enter_accessor_property( + &mut self, + prop: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !prop.decorators.is_empty() { + prop.decorators + .push(self.create_design_type_metadata(prop.type_annotation.as_ref(), ctx)); + } + } +} + +impl<'a> LegacyDecoratorMetadata<'a, '_> { + /// Collects enum type information for decorator metadata generation. + fn collect_enum_type(&mut self, decl: &TSEnumDeclaration<'a>, ctx: &TraverseCtx<'a>) { + let symbol_id = decl.id.symbol_id(); + + // Optimization: + // If the enum doesn't have any type references, that implies that no decorators + // refer to this enum, so there is no need to infer its type. + let has_type_reference = + ctx.scoping().get_resolved_references(symbol_id).any(Reference::is_type); + if has_type_reference { + let enum_type = Self::infer_enum_type(&decl.body.members); + self.enum_types.insert(symbol_id, enum_type); + } + } + + /// Infer the type of an enum based on its members + fn infer_enum_type(members: &[TSEnumMember<'a>]) -> EnumType { + let mut enum_type = EnumType::Object; + + for member in members { + if let Some(init) = &member.initializer { + match init { + Expression::StringLiteral(_) | Expression::TemplateLiteral(_) + if enum_type != EnumType::Number => + { + enum_type = EnumType::String; + } + // TS considers `+x`, `-x`, `~x` to be `Number` type, no matter what `x` is. + // All other unary expressions (`!x`, `void x`, `typeof x`, `delete x`) are illegal in enum initializers, + // so we can ignore those cases here and just say all `UnaryExpression`s are numeric. + // Bigint literals are also illegal in enum initializers, so we don't need to consider them here. + Expression::NumericLiteral(_) | Expression::UnaryExpression(_) + if enum_type != EnumType::String => + { + enum_type = EnumType::Number; + } + // For other expressions, we can't determine the type statically + _ => return EnumType::Object, + } + } else { + // No initializer means numeric (auto-incrementing from previous member) + if enum_type == EnumType::String { + return EnumType::Object; + } + enum_type = EnumType::Number; + } + } + + enum_type + } + + pub fn pop_method_metadata(&mut self) -> Option> { + self.method_metadata_stack.pop() + } + + pub fn pop_constructor_metadata(&mut self) -> Option> { + self.constructor_metadata_stack.pop() + } + + fn serialize_type_annotation( + &mut self, + type_annotation: Option<&ArenaBox<'a, TSTypeAnnotation<'a>>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if let Some(type_annotation) = type_annotation { + self.serialize_type_node(&type_annotation.type_annotation, ctx) + } else { + Self::global_object(ctx) + } + } + + /// Serializes a type node for use with decorator type metadata. + /// + /// Types are serialized in the following fashion: + /// - Void types point to "undefined" (e.g. "void 0") + /// - Function and Constructor types point to the global "Function" constructor. + /// - Interface types with a call or construct signature types point to the global + /// "Function" constructor. + /// - Array and Tuple types point to the global "Array" constructor. + /// - Type predicates and booleans point to the global "Boolean" constructor. + /// - String literal types and strings point to the global "String" constructor. + /// - Enum and number types point to the global "Number" constructor. + /// - Symbol types point to the global "Symbol" constructor. + /// - Type references to classes (or class-like variables) point to the constructor for the class. + /// - Anything else points to the global "Object" constructor. + fn serialize_type_node( + &mut self, + node: &TSType<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match &node { + TSType::TSVoidKeyword(_) + | TSType::TSUndefinedKeyword(_) + | TSType::TSNullKeyword(_) + | TSType::TSNeverKeyword(_) => ctx.ast.void_0(SPAN), + TSType::TSFunctionType(_) | TSType::TSConstructorType(_) => Self::global_function(ctx), + TSType::TSArrayType(_) | TSType::TSTupleType(_) => Self::global_array(ctx), + TSType::TSTypePredicate(t) => { + if t.asserts { + ctx.ast.void_0(SPAN) + } else { + Self::global_boolean(ctx) + } + } + TSType::TSBooleanKeyword(_) => Self::global_boolean(ctx), + TSType::TSTemplateLiteralType(_) | TSType::TSStringKeyword(_) => { + Self::global_string(ctx) + } + TSType::TSLiteralType(literal) => { + Self::serialize_literal_of_literal_type_node(&literal.literal, ctx) + } + TSType::TSNumberKeyword(_) => Self::global_number(ctx), + TSType::TSBigIntKeyword(_) => Self::global_bigint(ctx), + TSType::TSSymbolKeyword(_) => Self::global_symbol(ctx), + TSType::TSTypeReference(t) => { + self.serialize_type_reference_node(&t.type_name, ctx) + } + TSType::TSIntersectionType(t) => { + self.serialize_union_or_intersection_constituents(t.types.iter(), /* is_intersection */ true, ctx) + } + TSType::TSUnionType(t) => { + self.serialize_union_or_intersection_constituents(t.types.iter(), /* is_intersection */ false, ctx) + } + TSType::TSConditionalType(t) => { + self.serialize_union_or_intersection_constituents( + [&t.true_type, &t.false_type].into_iter(), + false, + ctx + ) + } + TSType::TSTypeOperatorType(operator) + if operator.operator == TSTypeOperatorOperator::Readonly => + { + self.serialize_type_node(&operator.type_annotation, ctx) + } + TSType::JSDocNullableType(t) => { + self.serialize_type_node(&t.type_annotation, ctx) + } + TSType::JSDocNonNullableType(t) => { + self.serialize_type_node(&t.type_annotation, ctx) + } + TSType::TSParenthesizedType(t) => { + self.serialize_type_node(&t.type_annotation, ctx) + } + TSType::TSObjectKeyword(_) + // Fallback to `Object` + | TSType::TSTypeQuery(_) | TSType::TSIndexedAccessType(_) | TSType::TSMappedType(_) + | TSType::TSTypeLiteral(_) | TSType::TSAnyKeyword(_) | TSType::TSUnknownKeyword(_) + | TSType::TSThisType(_) | TSType::TSImportType(_) | TSType::TSTypeOperatorType(_) + // Not allowed to be used in the start of type annotations, fallback to `Object` + | TSType::TSInferType(_) | TSType::TSIntrinsicKeyword(_) | TSType::TSNamedTupleMember(_) + | TSType::JSDocUnknownType(_) => Self::global_object(ctx), + } + } + + /// Serializes the type of a node for use with decorator type metadata. + fn serialize_parameters_types_of_node( + &mut self, + params: &FormalParameters<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let mut elements = + ctx.ast.vec_with_capacity(params.items.len() + usize::from(params.rest.is_some())); + elements.extend(params.items.iter().map(|param| { + ArrayExpressionElement::from(self.serialize_parameter_types_of_node(param, ctx)) + })); + + if let Some(rest) = ¶ms.rest { + elements.push(ArrayExpressionElement::from( + self.serialize_type_annotation(rest.argument.type_annotation.as_ref(), ctx), + )); + } + ctx.ast.expression_array(SPAN, elements) + } + + fn serialize_parameter_types_of_node( + &mut self, + param: &FormalParameter<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let type_annotation = match ¶m.pattern.kind { + BindingPatternKind::AssignmentPattern(pattern) => pattern.left.type_annotation.as_ref(), + _ => param.pattern.type_annotation.as_ref(), + }; + self.serialize_type_annotation(type_annotation, ctx) + } + + /// Serializes the return type of a node for use with decorator type metadata. + fn serialize_return_type_of_node( + &mut self, + func: &Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if func.r#async { + Self::global_promise(ctx) + } else if let Some(return_type) = &func.return_type { + self.serialize_type_node(&return_type.type_annotation, ctx) + } else { + ctx.ast.void_0(SPAN) + } + } + + /// `A.B` -> `typeof (_a$b = typeof A !== "undefined" && A.B) == "function" ? _a$b : Object` + /// + /// NOTE: This function only ports `unknown` part from [TypeScript](https://github.com/microsoft/TypeScript/blob/d85767abfd83880cea17cea70f9913e9c4496dcc/src/compiler/transformers/typeSerializer.ts#L499-L506) + fn serialize_type_reference_node( + &mut self, + name: &TSTypeName<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // Check if this is an enum type reference - if so, return the primitive type directly + if let TSTypeName::IdentifierReference(ident) = name { + let symbol_id = ctx.scoping().get_reference(ident.reference_id()).symbol_id(); + if let Some(symbol_id) = symbol_id + && let Some(enum_type) = self.enum_types.get(&symbol_id) + { + return match enum_type { + EnumType::String => Self::global_string(ctx), + EnumType::Number => Self::global_number(ctx), + EnumType::Object => Self::global_object(ctx), + }; + } + } + + let Some(serialized_type) = self.serialize_entity_name_as_expression_fallback(name, ctx) + else { + // Reach here means the referent is a type symbol, so use `Object` as fallback. + return Self::global_object(ctx); + }; + let binding = self.ctx.var_declarations.create_uid_var_based_on_node(&serialized_type, ctx); + let target = binding.create_write_target(ctx); + let assignment = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + target, + serialized_type, + ); + let type_of = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, assignment); + let right = ctx.ast.expression_string_literal(SPAN, "function", None); + let operator = BinaryOperator::StrictEquality; + let test = ctx.ast.expression_binary(SPAN, type_of, operator, right); + let consequent = binding.create_read_expression(ctx); + let alternate = Self::global_object(ctx); + ctx.ast.expression_conditional(SPAN, test, consequent, alternate) + } + + /// Serializes an entity name which may not exist at runtime, but whose access shouldn't throw + fn serialize_entity_name_as_expression_fallback( + &mut self, + name: &TSTypeName<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + match name { + // `A` -> `typeof A !== "undefined" && A` + TSTypeName::IdentifierReference(ident) => { + let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx); + if Self::is_type_symbol(binding.symbol_id, ctx) { + return None; + } + let flags = Self::get_reference_flags(&binding, ctx); + let ident1 = binding.create_expression(flags, ctx); + let ident2 = binding.create_expression(flags, ctx); + Some(Self::create_checked_value(ident1, ident2, ctx)) + } + TSTypeName::QualifiedName(qualified) => { + if let TSTypeName::IdentifierReference(ident) = &qualified.left { + // `A.B` -> `typeof A !== "undefined" && A.B` + let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx); + if Self::is_type_symbol(binding.symbol_id, ctx) { + return None; + } + let flags = Self::get_reference_flags(&binding, ctx); + let ident1 = binding.create_expression(flags, ctx); + let ident2 = binding.create_expression(flags, ctx); + let member = create_property_access(SPAN, ident1, &qualified.right.name, ctx); + Some(Self::create_checked_value(ident2, member, ctx)) + } else { + // `A.B.C` -> `typeof A !== "undefined" && (_a = A.B) !== void 0 && _a.C` + let mut left = + self.serialize_entity_name_as_expression_fallback(&qualified.left, ctx)?; + let binding = + self.ctx.var_declarations.create_uid_var_based_on_node(&left, ctx); + let Expression::LogicalExpression(logical) = &mut left else { unreachable!() }; + let right = logical.right.take_in(ctx.ast); + // `(_a = A.B)` + let right = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + binding.create_write_target(ctx), + right, + ); + // `(_a = A.B) !== void 0` + logical.right = ctx.ast.expression_binary( + SPAN, + right, + BinaryOperator::StrictInequality, + ctx.ast.void_0(SPAN), + ); + + let object = binding.create_read_expression(ctx); + let member = create_property_access(SPAN, object, &qualified.right.name, ctx); + Some(ctx.ast.expression_logical(SPAN, left, LogicalOperator::And, member)) + } + } + TSTypeName::ThisExpression(_) => None, + } + } + + fn serialize_literal_of_literal_type_node( + literal: &TSLiteral<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match literal { + TSLiteral::BooleanLiteral(_) => Self::global_boolean(ctx), + TSLiteral::NumericLiteral(_) => Self::global_number(ctx), + TSLiteral::BigIntLiteral(_) => Self::global_bigint(ctx), + TSLiteral::StringLiteral(_) | TSLiteral::TemplateLiteral(_) => Self::global_string(ctx), + TSLiteral::UnaryExpression(expr) => match expr.argument { + Expression::NumericLiteral(_) => Self::global_number(ctx), + Expression::StringLiteral(_) => Self::global_string(ctx), + // Cannot be a type annotation + _ => unreachable!(), + }, + } + } + + fn serialize_union_or_intersection_constituents<'t>( + &mut self, + types: impl Iterator>, + is_intersection: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> + where + 'a: 't, + { + let mut serialized_type = None; + + for t in types { + let t = t.without_parenthesized(); + match t { + TSType::TSNeverKeyword(_) => { + if is_intersection { + // Reduce to `never` in an intersection + return ctx.ast.void_0(SPAN); + } + // Elide `never` in a union + continue; + } + TSType::TSUnknownKeyword(_) => { + if !is_intersection { + // Reduce to `unknown` in a union + return Self::global_object(ctx); + } + // Elide `unknown` in an intersection + continue; + } + TSType::TSAnyKeyword(_) => { + return Self::global_object(ctx); + } + // Unlike TypeScript, we don't have a way to determine what the referent is, + // so return `Object` early, because once have a type reference, the final + // type is `Object` anyway. + TSType::TSTypeReference(_) => return Self::global_object(ctx), + _ => {} + } + + let serialized_constituent = self.serialize_type_node(t, ctx); + if matches!(&serialized_constituent, Expression::Identifier(ident) if ident.name == "Object") + { + // One of the individual is global object, return immediately + return serialized_constituent; + } + + // If there exists union that is not `void 0` expression, check if the the common type is identifier. + // anything more complex and we will just default to Object + if let Some(serialized_type) = &serialized_type { + // Different types + if !Self::equate_serialized_type_nodes(serialized_type, &serialized_constituent) { + return Self::global_object(ctx); + } + } else { + // Initialize the union type + serialized_type = Some(serialized_constituent); + } + } + + // If we were able to find common type, use it + serialized_type.unwrap_or_else(|| { + // Fallback is only hit if all union constituents are null/undefined/never + ctx.ast.void_0(SPAN) + }) + } + + /// Compares two serialized type nodes for equality. + /// + /// + #[inline] + fn equate_serialized_type_nodes(a: &Expression<'a>, b: &Expression<'a>) -> bool { + a.content_eq(b) + } + + #[inline] + fn is_type_symbol(symbol_id: Option, ctx: &TraverseCtx<'a>) -> bool { + symbol_id.is_some_and(|symbol_id| ctx.scoping().symbol_flags(symbol_id).is_type()) + } + + fn get_reference_flags( + binding: &MaybeBoundIdentifier<'a>, + ctx: &TraverseCtx<'a>, + ) -> ReferenceFlags { + if let Some(symbol_id) = binding.symbol_id { + // Type symbols have filtered out in [`serialize_entity_name_as_expression_fallback`]. + debug_assert!(ctx.scoping().symbol_flags(symbol_id).is_value()); + // `design::*type` would be called by `reflect-metadata` APIs, use `Read` flag + // to avoid TypeScript remove it because only used as types. + ReferenceFlags::Read + } else { + // Unresolved reference + ReferenceFlags::Type | ReferenceFlags::Read + } + } + + #[inline] + fn create_global_identifier(ident: &'static str, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + ctx.create_unbound_ident_expr(SPAN, Atom::new_const(ident), ReferenceFlags::Read) + } + + #[inline] + fn global_object(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Object", ctx) + } + + #[inline] + fn global_function(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Function", ctx) + } + + #[inline] + fn global_array(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Array", ctx) + } + + #[inline] + fn global_boolean(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Boolean", ctx) + } + + #[inline] + fn global_string(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("String", ctx) + } + + #[inline] + fn global_number(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Number", ctx) + } + + #[inline] + fn global_bigint(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("BigInt", ctx) + } + + #[inline] + fn global_symbol(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Symbol", ctx) + } + + #[inline] + fn global_promise(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Promise", ctx) + } + + /// Produces an expression that results in `right` if `left` is not undefined at runtime: + /// + /// ``` + /// typeof left !== "undefined" && right + /// ``` + /// + /// We use `typeof L !== "undefined"` (rather than `L !== undefined`) since `L` may not be declared. + /// It's acceptable for this expression to result in `false` at runtime, as the result is intended to be + /// further checked by any containing expression. + fn create_checked_value( + left: Expression<'a>, + right: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let operator = BinaryOperator::StrictInequality; + let undefined = ctx.ast.expression_string_literal(SPAN, "undefined", None); + let typeof_left = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, left); + let left_check = ctx.ast.expression_binary(SPAN, typeof_left, operator, undefined); + ctx.ast.expression_logical(SPAN, left_check, LogicalOperator::And, right) + } + + // `_metadata(key, value) + fn create_metadata( + &self, + key: &'a str, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let arguments = ctx.ast.vec_from_array([ + Argument::from(ctx.ast.expression_string_literal(SPAN, key, None)), + Argument::from(value), + ]); + self.ctx.helper_call_expr(Helper::DecorateMetadata, SPAN, arguments, ctx) + } + + // `_metadata(key, value) + fn create_metadata_decorate( + &self, + key: &'a str, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Decorator<'a> { + ctx.ast.decorator(SPAN, self.create_metadata(key, value, ctx)) + } + + /// `_metadata("design:type", type)` + fn create_design_type_metadata( + &mut self, + type_annotation: Option<&ArenaBox<'a, TSTypeAnnotation<'a>>>, + ctx: &mut TraverseCtx<'a>, + ) -> Decorator<'a> { + let serialized_type = self.serialize_type_annotation(type_annotation, ctx); + self.create_metadata_decorate("design:type", serialized_type, ctx) + } +} diff --git a/crates/swc_ecma_transformer/oxc/decorator/legacy/mod.rs b/crates/swc_ecma_transformer/oxc/decorator/legacy/mod.rs new file mode 100644 index 000000000000..2c6932390b1b --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/decorator/legacy/mod.rs @@ -0,0 +1,1252 @@ +//! Legacy decorator +//! +//! This plugin transforms legacy decorators by calling `_decorate` and `_decorateParam` helpers +//! to apply decorators. +//! +//! ## Examples +//! +//! Input: +//! ```ts +//! @dec +//! class Class { +//! @dec +//! prop = 0; +//! +//! @dec +//! method(@dec param) {} +//! } +//! ``` +//! +//! Output: +//! ```js +//! let Class = class Class { +//! prop = 0; +//! method(param) {} +//! }; +//! +//! _decorate([dec], Class.prototype, "method", null); +//! +//! _decorate([ +//! _decorateParam(0, dec) +//! ], Class.prototype, "method", null); +//! +//! Class = _decorate([dec], Class); +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [TypeScript Experimental Decorators](https://github.com/microsoft/TypeScript/blob/d85767abfd83880cea17cea70f9913e9c4496dcc/src/compiler/transformers/legacyDecorators.ts). +//! +//! For testing, we have copied over all legacy decorator test cases from [TypeScript](https://github.com/microsoft/TypeScript/blob/d85767abfd83880cea17cea70f9913e9c4496dcc/tests/cases/conformance/decorators), +//! where the test cases are located in `./tasks/transform_conformance/tests/legacy-decorators/test/fixtures`. +//! +//! ## References: +//! * TypeScript Experimental Decorators documentation: + +mod metadata; + +use std::mem; + +use oxc_allocator::{Address, GetAddress, TakeIn, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_ast_visit::{Visit, VisitMut}; +use oxc_data_structures::stack::NonEmptyStack; +use oxc_semantic::{ScopeFlags, SymbolFlags}; +use oxc_span::SPAN; +use oxc_syntax::operator::AssignmentOperator; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse}; +use rustc_hash::FxHashMap; + +use crate::{ + Helper, + context::{TransformCtx, TraverseCtx}, + state::TransformState, + utils::ast_builder::{create_assignment, create_prototype_member}, +}; +use metadata::LegacyDecoratorMetadata; + +struct ClassDecoratedData<'a> { + // Class binding. When a class is without binding, it will be `_default`, + binding: BoundIdentifier<'a>, + // Alias binding exist when the class body contains a reference that refers to class itself. + alias_binding: Option>, +} + +/// Class decorations state for the current class being processed. +#[derive(Default)] +struct ClassDecorations<'a> { + /// Flag indicating whether the current class needs to transform or not, + /// `false` if the class is an expression or `declare`. + should_transform: bool, + /// Decoration statements accumulated for the current class. + /// These will be applied when the class processing is complete. + decoration_stmts: Vec>, + /// Binding for the current class being processed. + /// Generated on-demand when the first decorator needs it. + class_binding: Option>, + /// Flag indicating whether the current class has a private `in` expression in any decorator. + /// This affects where decorations are placed (in static block vs after class). + class_has_private_in_expression_in_decorator: bool, +} + +impl ClassDecorations<'_> { + fn with_should_transform(mut self, should_transform: bool) -> Self { + self.should_transform = should_transform; + self + } +} + +pub struct LegacyDecorator<'a, 'ctx> { + emit_decorator_metadata: bool, + metadata: LegacyDecoratorMetadata<'a, 'ctx>, + /// Decorated class data exists when a class or constructor is decorated. + /// + /// The data assigned in [`Self::transform_class`] and used in places where statements contain + /// a decorated class declaration because decorated class needs to transform into `let c = class c {}`. + /// The reason we why don't transform class in [`Self::exit_statement`] is the decorator transform + /// must run first. Since the `class-properties` plugin transforms class in `exit_class`, so that + /// we have to transforms decorators to `exit_class` otherwise after class is being transformed by + /// `class-properties` plugin, the decorators' nodes might be lost. + class_decorated_data: Option>, + /// Transformed decorators, they will be inserted in the statements at [`Self::exit_class_at_end`]. + decorations: FxHashMap>>, + /// Stack for managing nested class decoration state. + /// Each level represents the decoration state for a class in the hierarchy, + /// with the top being the currently processed class. + class_decorations_stack: NonEmptyStack>, + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> LegacyDecorator<'a, 'ctx> { + pub fn new(emit_decorator_metadata: bool, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + emit_decorator_metadata, + metadata: LegacyDecoratorMetadata::new(ctx), + class_decorated_data: None, + decorations: FxHashMap::default(), + class_decorations_stack: NonEmptyStack::new(ClassDecorations::default()), + ctx, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for LegacyDecorator<'a, '_> { + #[inline] + fn exit_program(&mut self, node: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.emit_decorator_metadata { + self.metadata.exit_program(node, ctx); + } + + debug_assert!( + self.class_decorations_stack.is_exhausted(), + "All class decorations should have been popped." + ); + } + + #[inline] + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if self.emit_decorator_metadata { + self.metadata.enter_statement(stmt, ctx); + } + } + + #[inline] + fn enter_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.class_decorations_stack.push( + ClassDecorations::default() + .with_should_transform(!(class.is_expression() || class.declare)), + ); + + if self.emit_decorator_metadata { + self.metadata.enter_class(class, ctx); + } + } + + #[inline] + fn exit_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.transform_class(class, ctx); + } + + // `#[inline]` because this is a hot path + #[inline] + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + match stmt { + Statement::ClassDeclaration(_) => self.transform_class_statement(stmt, ctx), + Statement::ExportNamedDeclaration(_) => { + self.transform_export_named_class(stmt, ctx); + } + Statement::ExportDefaultDeclaration(_) => { + self.transform_export_default_class(stmt, ctx); + } + _ => {} + } + } + + #[inline] + fn enter_method_definition( + &mut self, + node: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.emit_decorator_metadata { + self.metadata.enter_method_definition(node, ctx); + } + } + + #[inline] + fn enter_accessor_property( + &mut self, + node: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.emit_decorator_metadata { + self.metadata.enter_accessor_property(node, ctx); + } + } + + #[inline] + fn enter_property_definition( + &mut self, + node: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.emit_decorator_metadata { + self.metadata.enter_property_definition(node, ctx); + } + } + + #[inline] + fn exit_method_definition( + &mut self, + method: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // `constructor` will handle in `transform_decorators_of_class_and_constructor`. + if method.kind.is_constructor() { + return; + } + + if let Some(decorations) = self.get_all_decorators_of_class_method(method, ctx) { + // We emit `null` here to indicate to `_decorate` that it can invoke `Object.getOwnPropertyDescriptor` directly. + let descriptor = ctx.ast.expression_null_literal(SPAN); + self.handle_decorated_class_element( + method.r#static, + &mut method.key, + descriptor, + decorations, + ctx, + ); + } + } + + #[inline] + fn exit_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if prop.decorators.is_empty() { + return; + } + + let decorations = + Self::convert_decorators_to_array_expression(prop.decorators.drain(..), ctx); + + // We emit `void 0` here to indicate to `_decorate` that it can invoke `Object.defineProperty` directly. + let descriptor = ctx.ast.void_0(SPAN); + self.handle_decorated_class_element( + prop.r#static, + &mut prop.key, + descriptor, + decorations, + ctx, + ); + } + + #[inline] + fn exit_accessor_property( + &mut self, + accessor: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if accessor.decorators.is_empty() { + return; + } + + let decorations = + Self::convert_decorators_to_array_expression(accessor.decorators.drain(..), ctx); + // We emit `null` here to indicate to `_decorate` that it can invoke `Object.getOwnPropertyDescriptor` directly. + let descriptor = ctx.ast.expression_null_literal(SPAN); + self.handle_decorated_class_element( + accessor.r#static, + &mut accessor.key, + descriptor, + decorations, + ctx, + ); + } + + fn enter_decorator(&mut self, node: &mut Decorator<'a>, _ctx: &mut TraverseCtx<'a>) { + let current_class = self.class_decorations_stack.last_mut(); + if current_class.should_transform + && !current_class.class_has_private_in_expression_in_decorator + { + current_class.class_has_private_in_expression_in_decorator = + PrivateInExpressionDetector::has_private_in_expression(&node.expression); + } + } +} + +impl<'a> LegacyDecorator<'a, '_> { + /// Helper method to handle a decorated class element (method, property, or accessor). + /// Accumulates decoration statements in the current decoration stack. + fn handle_decorated_class_element( + &mut self, + is_static: bool, + key: &mut PropertyKey<'a>, + descriptor: Expression<'a>, + decorations: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let current_class = self.class_decorations_stack.last_mut(); + + if !current_class.should_transform { + return; + } + + // Get current class binding from stack + let class_binding = current_class.class_binding.get_or_insert_with(|| { + let Ancestor::ClassBody(class) = ctx.ancestor(1) else { + unreachable!("The grandparent of a class element is always a class."); + }; + if let Some(ident) = class.id() { + BoundIdentifier::from_binding_ident(ident) + } else { + ctx.generate_uid_in_current_scope("default", SymbolFlags::Class) + } + }); + + let prefix = Self::get_class_member_prefix(class_binding, is_static, ctx); + let name = self.get_name_of_property_key(key, ctx); + let decorator_stmt = self.create_decorator(decorations, prefix, name, descriptor, ctx); + + // Push to current decoration stack + self.class_decorations_stack.last_mut().decoration_stmts.push(decorator_stmt); + } + /// Transforms a statement that is a class declaration + /// + /// + /// Input: + /// ```ts + /// @dec + /// class Class { + /// method(@dec param) {} + /// } + /// ``` + /// + /// Output: + /// ```js + /// let Class = class Class { + /// method(param) { } + /// }; + /// + /// _decorate([ + /// _decorateParam(0, dec) + /// ], Class.prototype, "method", null); + /// + /// Class = _decorate([ + /// dec + /// ], Class); + /// ``` + // `#[inline]` so that compiler sees that `stmt` is a `Statement::ClassDeclaration`. + #[inline] + fn transform_class_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let Statement::ClassDeclaration(class) = stmt else { unreachable!() }; + + let Some(ClassDecoratedData { binding, alias_binding }) = self.class_decorated_data.take() + else { + return; + }; + + let new_stmt = + Self::transform_class_decorated(class, &binding, alias_binding.as_ref(), ctx); + + self.ctx.statement_injector.move_insertions(stmt, &new_stmt); + *stmt = new_stmt; + } + + /// Transforms a statement that is a export default class declaration + /// + /// Input: + /// ```ts + /// @dec + /// export default class Class { + /// method(@dec param) {} + /// } + /// ``` + /// + /// Output: + /// ```js + /// let Class = class Class { + /// method(param) { } + /// }; + /// + /// _decorate([ + /// _decorateParam(0, dec) + /// ], Class.prototype, "method", null); + /// + /// Class = _decorate([ + /// dec + /// ], Class); + /// + /// export default Class; + /// ``` + // `#[inline]` so that compiler sees that `stmt` is a `Statement::ExportDefaultDeclaration`. + #[inline] + fn transform_export_default_class( + &mut self, + stmt: &mut Statement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Statement::ExportDefaultDeclaration(export) = stmt else { unreachable!() }; + let ExportDefaultDeclarationKind::ClassDeclaration(class) = &mut export.declaration else { + return; + }; + let Some(ClassDecoratedData { binding, alias_binding }) = self.class_decorated_data.take() + else { + return; + }; + + let new_stmt = + Self::transform_class_decorated(class, &binding, alias_binding.as_ref(), ctx); + + // `export default Class` + let export_default_class_reference = + Self::create_export_default_class_reference(&binding, ctx); + self.ctx.statement_injector.move_insertions(stmt, &new_stmt); + self.ctx.statement_injector.insert_after(&new_stmt, export_default_class_reference); + *stmt = new_stmt; + } + + /// Transforms a statement that is a export named class declaration + /// + /// Input: + /// ```ts + /// @dec + /// export class Class { + /// method(@dec param) {} + /// } + /// ``` + /// + /// Output: + /// ```js + /// let Class = class Class { + /// method(param) { } + /// }; + /// + /// _decorate([ + /// _decorateParam(0, dec) + /// ], Class.prototype, "method", null); + /// + /// Class = _decorate([ + /// dec + /// ], Class); + /// + /// export { Class }; + /// ``` + // `#[inline]` so that compiler sees that `stmt` is a `Statement::ExportNamedDeclaration`. + #[inline] + fn transform_export_named_class( + &mut self, + stmt: &mut Statement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Statement::ExportNamedDeclaration(export) = stmt else { unreachable!() }; + let Some(Declaration::ClassDeclaration(class)) = &mut export.declaration else { return }; + + let Some(ClassDecoratedData { binding, alias_binding }) = self.class_decorated_data.take() + else { + return; + }; + + let new_stmt = + Self::transform_class_decorated(class, &binding, alias_binding.as_ref(), ctx); + + // `export { Class }` + let export_class_reference = Self::create_export_named_class_reference(&binding, ctx); + self.ctx.statement_injector.move_insertions(stmt, &new_stmt); + self.ctx.statement_injector.insert_after(&new_stmt, export_class_reference); + *stmt = new_stmt; + } + + fn transform_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + let current_class_decorations = self.class_decorations_stack.pop(); + + // Legacy decorator does not allow in class expression. + if current_class_decorations.should_transform { + let class_or_constructor_parameter_is_decorated = + Self::check_class_has_decorated(class); + + if class_or_constructor_parameter_is_decorated { + self.transform_class_declaration_with_class_decorators( + class, + current_class_decorations, + ctx, + ); + return; + } else if !current_class_decorations.decoration_stmts.is_empty() { + self.transform_class_declaration_without_class_decorators( + class, + current_class_decorations, + ctx, + ); + } + } else { + debug_assert!( + current_class_decorations.class_binding.is_none(), + "Legacy decorator does not allow class expression, so that it should not have class binding." + ); + } + + debug_assert!( + !self.emit_decorator_metadata || self.metadata.pop_constructor_metadata().is_none(), + "`pop_constructor_metadata` should be `None` because there are no class decorators, so no metadata was generated." + ); + } + + /// Transforms a decorated class declaration and appends the resulting statements. If + /// the class requires an alias to avoid issues with double-binding, the alias is returned. + fn transform_class_declaration_with_class_decorators( + &mut self, + class: &mut Class<'a>, + current_class_decorations: ClassDecorations<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // When we emit an ES6 class that has a class decorator, we must tailor the + // emit to certain specific cases. + // + // In the simplest case, we emit the class declaration as a let declaration, and + // evaluate decorators after the close of the class body: + // + // [Example 1] + // --------------------------------------------------------------------- + // TypeScript | Javascript + // --------------------------------------------------------------------- + // @dec | let C = class C { + // class C { | } + // } | C = _decorate([dec], C); + // --------------------------------------------------------------------- + // @dec | let C = class C { + // export class C { | } + // } | C = _decorate([dec], C); + // | export { C }; + // --------------------------------------------------------------------- + // + // If a class declaration contains a reference to itself *inside* of the class body, + // this introduces two bindings to the class: One outside of the class body, and one + // inside of the class body. If we apply decorators as in [Example 1] above, there + // is the possibility that the decorator `dec` will return a new value for the + // constructor, which would result in the binding inside of the class no longer + // pointing to the same reference as the binding outside of the class. + // + // As a result, we must instead rewrite all references to the class *inside* of the + // class body to instead point to a local temporary alias for the class: + // + // [Example 2] + // --------------------------------------------------------------------- + // TypeScript | Javascript + // --------------------------------------------------------------------- + // @dec | let C = C_1 = class C { + // class C { | static x() { return C_1.y; } + // static x() { return C.y; } | } + // static y = 1; | C.y = 1; + // } | C = C_1 = _decorate([dec], C); + // | var C_1; + // --------------------------------------------------------------------- + // @dec | let C = class C { + // export class C { | static x() { return C_1.y; } + // static x() { return C.y; } | } + // static y = 1; | C.y = 1; + // } | C = C_1 = _decorate([dec], C); + // | export { C }; + // | var C_1; + // --------------------------------------------------------------------- + // + // If a class declaration is the default export of a module, we instead emit + // the export after the decorated declaration: + // + // [Example 3] + // --------------------------------------------------------------------- + // TypeScript | Javascript + // --------------------------------------------------------------------- + // @dec | let default_1 = class { + // export default class { | } + // } | default_1 = _decorate([dec], default_1); + // | export default default_1; + // --------------------------------------------------------------------- + // @dec | let C = class C { + // export default class C { | } + // } | C = _decorate([dec], C); + // | export default C; + // --------------------------------------------------------------------- + // + // If the class declaration is the default export and a reference to itself + // inside of the class body, we must emit both an alias for the class *and* + // move the export after the declaration: + // + // [Example 4] + // --------------------------------------------------------------------- + // TypeScript | Javascript + // --------------------------------------------------------------------- + // @dec | let C = class C { + // export default class C { | static x() { return C_1.y; } + // static x() { return C.y; } | } + // static y = 1; | C.y = 1; + // } | C = C_1 = _decorate([dec], C); + // | export default C; + // | var C_1; + // --------------------------------------------------------------------- + // + + // TODO(improve-on-typescript): we can take the class id without keeping it as-is. + // Now: `class C {}` -> `let C = class C {}` + // After: `class C {}` -> `let C = class {}` + let class_binding = class.id.as_ref().map(|ident| { + let new_class_binding = + ctx.generate_binding(ident.name, class.scope_id(), SymbolFlags::Class); + let old_class_symbol_id = ident.symbol_id.replace(Some(new_class_binding.symbol_id)); + let old_class_symbol_id = old_class_symbol_id.expect("class always has a symbol id"); + + *ctx.scoping_mut().symbol_flags_mut(old_class_symbol_id) = + SymbolFlags::BlockScopedVariable; + BoundIdentifier::new(ident.name, old_class_symbol_id) + }); + let class_alias_binding = class_binding.as_ref().and_then(|id| { + ClassReferenceChanger::new(id.clone(), ctx, self.ctx) + .get_class_alias_if_needed(&mut class.body) + }); + + let ClassDecorations { + class_binding: class_binding_tmp, + mut decoration_stmts, + class_has_private_in_expression_in_decorator, + should_transform: _, + } = current_class_decorations; + + let class_binding = class_binding.unwrap_or_else(|| { + // `class_binding_tmp` maybe already generated a default class binding for unnamed classes, so use it. + class_binding_tmp + .unwrap_or_else(|| ctx.generate_uid_in_current_scope("default", SymbolFlags::Class)) + }); + + let constructor_decoration = self.transform_decorators_of_class_and_constructor( + class, + &class_binding, + class_alias_binding.as_ref(), + ctx, + ); + + let class_alias_with_this_assignment = if self.ctx.is_class_properties_plugin_enabled { + None + } else { + // If we're emitting to ES2022 or later then we need to reassign the class alias before + // static initializers are evaluated. + // + class_alias_binding.as_ref().and_then(|class_alias_binding| { + let has_static_field_or_block = class.body.body.iter().any(|element| { + matches!(element, ClassElement::StaticBlock(_)) + || matches!(element, ClassElement::PropertyDefinition(prop) + if prop.r#static + ) + }); + + if has_static_field_or_block { + // `_Class = this`; + let class_alias_with_this_assignment = ctx.ast.statement_expression( + SPAN, + create_assignment(class_alias_binding, ctx.ast.expression_this(SPAN), ctx), + ); + let body = ctx.ast.vec1(class_alias_with_this_assignment); + let scope_id = ctx.create_child_scope_of_current(ScopeFlags::ClassStaticBlock); + let element = + ctx.ast.class_element_static_block_with_scope_id(SPAN, body, scope_id); + Some(element) + } else { + None + } + }) + }; + + if class_has_private_in_expression_in_decorator { + let decorations = mem::take(&mut decoration_stmts); + Self::insert_decorations_into_class_static_block(class, decorations, ctx); + } else { + let address = match ctx.parent() { + Ancestor::ExportDefaultDeclarationDeclaration(_) + | Ancestor::ExportNamedDeclarationDeclaration(_) => ctx.parent().address(), + // `Class` is always stored in a `Box`, so has a stable memory location + _ => Address::from_ref(class), + }; + + decoration_stmts.push(constructor_decoration); + self.decorations.entry(address).or_default().append(&mut decoration_stmts); + self.class_decorated_data = Some(ClassDecoratedData { + binding: class_binding, + // If the class alias has reassigned to `this` in the static block, then + // don't assign `class` to the class alias again. + // + // * class_alias_with_this_assignment is `None`: + // `Class = _Class = class Class {}` + // * class_alias_with_this_assignment is `Some`: + // `Class = class Class { static { _Class = this; } }` + alias_binding: if class_alias_with_this_assignment.is_none() { + class_alias_binding + } else { + None + }, + }); + } + + if let Some(class_alias_with_this_assignment) = class_alias_with_this_assignment { + class.body.body.insert(0, class_alias_with_this_assignment); + } + } + + /// Transform class to a [`VariableDeclarator`], whose binding name is the same as class. + /// + /// * `alias_binding` is `None`: `class C {}` -> `let C = class C {}` + /// * `alias_binding` is `Some`: `class C {}` -> `let C = _C = class C {}` + fn transform_class_decorated( + class: &mut Class<'a>, + binding: &BoundIdentifier<'a>, + alias_binding: Option<&BoundIdentifier<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let span = class.span; + class.r#type = ClassType::ClassExpression; + let initializer = Self::get_class_initializer( + Expression::ClassExpression(class.take_in_box(ctx.ast)), + alias_binding, + ctx, + ); + let declarator = ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Let, + binding.create_binding_pattern(ctx), + Some(initializer), + false, + ); + let var_declaration = ctx.ast.declaration_variable( + span, + VariableDeclarationKind::Let, + ctx.ast.vec1(declarator), + false, + ); + Statement::from(var_declaration) + } + + /// Transforms a non-decorated class declaration. + fn transform_class_declaration_without_class_decorators( + &mut self, + class: &mut Class<'a>, + current_class_decorations: ClassDecorations<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let ClassDecorations { + class_binding, + mut decoration_stmts, + class_has_private_in_expression_in_decorator, + should_transform: _, + } = current_class_decorations; + + let Some(class_binding) = class_binding else { + unreachable!( + "Always has a class binding because there are decorators in class elements, + so that it has been added in `handle_decorated_class_element`" + ); + }; + + // No class id, add one by using the class binding + if class.id.is_none() { + class.id = Some(class_binding.create_binding_identifier(ctx)); + } + + if class_has_private_in_expression_in_decorator { + Self::insert_decorations_into_class_static_block(class, decoration_stmts, ctx); + } else { + let stmt_address = match ctx.parent() { + parent @ (Ancestor::ExportDefaultDeclarationDeclaration(_) + | Ancestor::ExportNamedDeclarationDeclaration(_)) => parent.address(), + // `Class` is always stored in a `Box`, so has a stable memory location + _ => Address::from_ref(class), + }; + self.decorations.entry(stmt_address).or_default().append(&mut decoration_stmts); + } + } + + /// Transform the decorators of class and constructor method. + /// + /// Input: + /// ```ts + /// @dec + /// class Class { + /// method(@dec param) {} + /// } + /// ``` + /// + /// These decorators transform into: + /// ``` + /// _decorate([ + /// _decorateParam(0, dec) + /// ], Class.prototype, "method", null); + /// + /// Class = _decorate([ + /// dec + /// ], Class); + /// ``` + fn transform_decorators_of_class_and_constructor( + &mut self, + class: &mut Class<'a>, + class_binding: &BoundIdentifier<'a>, + class_alias_binding: Option<&BoundIdentifier<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + // Find first constructor method from the class + let constructor = class.body.body.iter_mut().find_map(|element| match element { + ClassElement::MethodDefinition(method) if method.kind.is_constructor() => Some(method), + _ => None, + }); + + let decorations = if let Some(constructor) = constructor { + // Constructor cannot have decorators, swap decorators of class and constructor to use + // `get_all_decorators_of_class_method` to get all decorators of the class and constructor params + mem::swap(&mut class.decorators, &mut constructor.decorators); + // constructor.decorators + self.get_all_decorators_of_class_method(constructor, ctx) + .expect("At least one decorator") + } else { + debug_assert!( + !self.emit_decorator_metadata || self.metadata.pop_constructor_metadata().is_none(), + "`pop_constructor_metadata` should be `None` because there is no `constructor`, so no metadata was generated." + ); + Self::convert_decorators_to_array_expression(class.decorators.drain(..), ctx) + }; + + // `Class = _decorate(decorations, Class)` + let arguments = ctx.ast.vec_from_array([ + Argument::from(decorations), + Argument::from(class_binding.create_read_expression(ctx)), + ]); + let helper = self.ctx.helper_call_expr(Helper::Decorate, SPAN, arguments, ctx); + let operator = AssignmentOperator::Assign; + let left = class_binding.create_write_target(ctx); + let right = Self::get_class_initializer(helper, class_alias_binding, ctx); + let assignment = ctx.ast.expression_assignment(SPAN, operator, left, right); + ctx.ast.statement_expression(SPAN, assignment) + } + + /// Insert all decorations into a static block of a class because there is a + /// private-in expression in the decorator. + /// + /// Input: + /// ```ts + /// class Class { + /// #a =0; + /// @(#a in Class ? dec() : dec2()) + /// prop = 0; + /// } + /// ``` + /// + /// Output: + /// ```js + /// class Class { + /// #a = 0; + /// prop = 0; + /// static { + /// _decorate([ + /// (#a in Class ? dec() : dec2()) + /// ], Class.prototype, "prop", void 0); + /// } + /// } + /// ``` + fn insert_decorations_into_class_static_block( + class: &mut Class<'a>, + decorations: Vec>, + ctx: &mut TraverseCtx<'a>, + ) { + let scope_id = ctx.create_child_scope(class.scope_id(), ScopeFlags::ClassStaticBlock); + let decorations = ctx.ast.vec_from_iter(decorations); + let element = ctx.ast.class_element_static_block_with_scope_id(SPAN, decorations, scope_id); + class.body.body.push(element); + } + + /// Transforms the decorators of the parameters of a class method. + #[expect(clippy::cast_precision_loss)] + fn transform_decorators_of_parameters( + &self, + decorations: &mut ArenaVec<'a, ArrayExpressionElement<'a>>, + params: &mut FormalParameters<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + for (index, param) in &mut params.items.iter_mut().enumerate() { + if param.decorators.is_empty() { + continue; + } + decorations.extend(param.decorators.drain(..).map(|decorator| { + // (index, decorator) + let index = ctx.ast.expression_numeric_literal( + SPAN, + index as f64, + None, + NumberBase::Decimal, + ); + let arguments = ctx + .ast + .vec_from_array([Argument::from(index), Argument::from(decorator.expression)]); + // _decorateParam(index, decorator) + ArrayExpressionElement::from(self.ctx.helper_call_expr( + Helper::DecorateParam, + decorator.span, + arguments, + ctx, + )) + })); + } + } + + /// Injects the class decorator statements after class-properties plugin has run, ensuring that + /// all transformed fields are injected before the class decorator statements. + pub fn exit_class_at_end(&mut self, _class: &mut Class<'a>, _ctx: &mut TraverseCtx<'a>) { + for (address, stmts) in mem::take(&mut self.decorations) { + self.ctx.statement_injector.insert_many_after(&address, stmts); + } + } + + /// Converts a vec of [`Decorator`] to [`Expression::ArrayExpression`]. + fn convert_decorators_to_array_expression( + decorators_iter: impl Iterator>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let decorations = ctx.ast.vec_from_iter( + decorators_iter.map(|decorator| ArrayExpressionElement::from(decorator.expression)), + ); + ctx.ast.expression_array(SPAN, decorations) + } + + /// Get all decorators of a class method. + /// + /// ```ts + /// class Class { + /// @dec + /// method(@dec param) {} + /// } + /// ``` + /// + /// Returns: + /// ```js + /// [ + /// dec, + /// _decorateParam(0, dec) + /// ] + /// ``` + fn get_all_decorators_of_class_method( + &mut self, + method: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let params = &mut method.value.params; + let param_decoration_count = + params.items.iter().fold(0, |acc, param| acc + param.decorators.len()); + let method_decoration_count = method.decorators.len() + param_decoration_count; + + if method_decoration_count == 0 { + if self.emit_decorator_metadata { + if method.kind.is_constructor() { + debug_assert!( + self.metadata.pop_constructor_metadata().is_none(), + "No method decorators, so `pop_constructor_metadata` should be `None`" + ); + } else { + debug_assert!( + self.metadata.pop_method_metadata().is_none(), + "No method decorators, so `pop_method_metadata` should be `None`" + ); + } + } + return None; + } + + let mut decorations = ctx.ast.vec_with_capacity(method_decoration_count); + + // Method decorators should always be injected before all other decorators + decorations.extend( + method + .decorators + .take_in(ctx.ast) + .into_iter() + .map(|decorator| ArrayExpressionElement::from(decorator.expression)), + ); + + // The decorators of params are always inserted at the end if any. + if param_decoration_count > 0 { + self.transform_decorators_of_parameters(&mut decorations, params, ctx); + } + + if self.emit_decorator_metadata { + // `decorateMetadata` should always be injected after param decorators + if method.kind.is_constructor() { + if let Some(metadata) = self.metadata.pop_constructor_metadata() { + decorations.push(ArrayExpressionElement::from(metadata)); + } + } else if let Some(metadata) = self.metadata.pop_method_metadata() { + decorations.push(ArrayExpressionElement::from(metadata.r#type)); + decorations.push(ArrayExpressionElement::from(metadata.param_types)); + if let Some(return_type) = metadata.return_type { + decorations.push(ArrayExpressionElement::from(return_type)); + } + } + } + + Some(ctx.ast.expression_array(SPAN, decorations)) + } + + /// * class_alias_binding is `Some`: `Class = _Class = expr` + /// * class_alias_binding is `None`: `Class = expr` + fn get_class_initializer( + expr: Expression<'a>, + class_alias_binding: Option<&BoundIdentifier<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if let Some(class_alias_binding) = class_alias_binding { + let left = class_alias_binding.create_write_target(ctx); + ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, left, expr) + } else { + expr + } + } + + /// Check if a class or its constructor parameters have decorators. + fn check_class_has_decorated(class: &Class<'a>) -> bool { + if !class.decorators.is_empty() { + return true; + } + + class.body.body.iter().any(|element| { + matches!(element, + ClassElement::MethodDefinition(method) if method.kind.is_constructor() && + Self::class_method_parameter_is_decorated(&method.value) + ) + }) + } + + /// Check if a class method parameter is decorated. + fn class_method_parameter_is_decorated(func: &Function<'a>) -> bool { + func.params.items.iter().any(|param| !param.decorators.is_empty()) + } + + /// * is_static is `true`: `Class` + /// * is_static is `false`: `Class.prototype` + fn get_class_member_prefix( + class_binding: &BoundIdentifier<'a>, + is_static: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let ident = class_binding.create_read_expression(ctx); + if is_static { ident } else { create_prototype_member(ident, ctx) } + } + + /// Get the name of the property key. + /// + /// * StaticIdentifier: `a = 0;` -> `a` + /// * PrivateIdentifier: `#a = 0;` -> `""` + /// * Computed property key: + /// * Copiable key: + /// * NumericLiteral: `[1] = 0;` -> `1` + /// * StringLiteral: `["a"] = 0;` -> `"a"` + /// * TemplateLiteral: `[`a`] = 0;` -> `a` + /// * NullLiteral: `[null] = 0;` -> `null` + /// * Non-copiable key: + /// * `[a()] = 0;` mutates the key to `[_a = a()] = 0;` and returns `_a` + fn get_name_of_property_key( + &self, + key: &mut PropertyKey<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match key { + PropertyKey::StaticIdentifier(ident) => { + ctx.ast.expression_string_literal(SPAN, ident.name, None) + } + // Legacy decorators do not support private key + PropertyKey::PrivateIdentifier(_) => ctx.ast.expression_string_literal(SPAN, "", None), + // Copiable literals + PropertyKey::NumericLiteral(literal) => { + Expression::NumericLiteral(ctx.ast.alloc(literal.clone())) + } + PropertyKey::StringLiteral(literal) => { + Expression::StringLiteral(ctx.ast.alloc(literal.clone())) + } + PropertyKey::TemplateLiteral(literal) if literal.expressions.is_empty() => { + let quasis = ctx.ast.vec_from_iter(literal.quasis.iter().cloned()); + ctx.ast.expression_template_literal(SPAN, quasis, ctx.ast.vec()) + } + PropertyKey::NullLiteral(_) => ctx.ast.expression_null_literal(SPAN), + _ => { + // ```ts + // Input: + // class Test { + // static [a()] = 0; + // } + + // Output: + // ```js + // let _a; + // class Test { + // static [_a = a()] = 0; + // ``` + + // Create a unique binding for the computed property key, and insert it outside of the class + let binding = self.ctx.var_declarations.create_uid_var_based_on_node(key, ctx); + let operator = AssignmentOperator::Assign; + let left = binding.create_read_write_target(ctx); + let right = key.to_expression_mut().take_in(ctx.ast); + let key_expr = ctx.ast.expression_assignment(SPAN, operator, left, right); + *key = PropertyKey::from(key_expr); + binding.create_read_expression(ctx) + } + } + } + + /// `_decorator([...decorators], Class, name, descriptor)` + fn create_decorator( + &self, + decorations: Expression<'a>, + prefix: Expression<'a>, + name: Expression<'a>, + descriptor: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let arguments = ctx.ast.vec_from_array([ + Argument::from(decorations), + Argument::from(prefix), + Argument::from(name), + Argument::from(descriptor), + ]); + let helper = self.ctx.helper_call_expr(Helper::Decorate, SPAN, arguments, ctx); + ctx.ast.statement_expression(SPAN, helper) + } + + /// `export default Class` + fn create_export_default_class_reference( + class_binding: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let export_default_class_reference = ctx.ast.module_declaration_export_default_declaration( + SPAN, + ExportDefaultDeclarationKind::Identifier( + ctx.ast.alloc(class_binding.create_read_reference(ctx)), + ), + ); + Statement::from(export_default_class_reference) + } + + /// `export { Class }` + fn create_export_named_class_reference( + class_binding: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let kind = ImportOrExportKind::Value; + let local = ModuleExportName::IdentifierReference(class_binding.create_read_reference(ctx)); + let exported = ctx.ast.module_export_name_identifier_name(SPAN, class_binding.name); + let specifiers = ctx.ast.vec1(ctx.ast.export_specifier(SPAN, local, exported, kind)); + let export_class_reference = ctx + .ast + .module_declaration_export_named_declaration(SPAN, None, specifiers, None, kind, NONE); + Statement::from(export_class_reference) + } +} + +/// Visitor to detect if a private-in expression is present in a decorator +#[derive(Default)] +struct PrivateInExpressionDetector { + has_private_in_expression: bool, +} + +impl Visit<'_> for PrivateInExpressionDetector { + fn visit_private_in_expression(&mut self, _it: &PrivateInExpression<'_>) { + self.has_private_in_expression = true; + } + + fn visit_decorators(&mut self, decorators: &ArenaVec<'_, Decorator<'_>>) { + for decorator in decorators { + self.visit_expression(&decorator.expression); + // Early exit if a private-in expression is found + if self.has_private_in_expression { + break; + } + } + } +} + +impl PrivateInExpressionDetector { + fn has_private_in_expression(expression: &Expression<'_>) -> bool { + let mut detector = Self::default(); + detector.visit_expression(expression); + detector.has_private_in_expression + } +} + +/// Visitor to change references to the class to a local alias +/// +struct ClassReferenceChanger<'a, 'ctx> { + class_binding: BoundIdentifier<'a>, + // `Some` if there are references to the class inside the class body + class_alias_binding: Option>, + ctx: &'ctx mut TraverseCtx<'a>, + transformer_ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> ClassReferenceChanger<'a, 'ctx> { + fn new( + class_binding: BoundIdentifier<'a>, + ctx: &'ctx mut TraverseCtx<'a>, + transformer_ctx: &'ctx TransformCtx<'a>, + ) -> Self { + Self { class_binding, class_alias_binding: None, ctx, transformer_ctx } + } + + fn get_class_alias_if_needed( + mut self, + class: &mut ClassBody<'a>, + ) -> Option> { + self.visit_class_body(class); + self.class_alias_binding + } +} + +impl<'a> VisitMut<'a> for ClassReferenceChanger<'a, '_> { + #[inline] + fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) { + if self.is_class_reference(ident) { + *ident = self.get_alias_ident_reference(); + } + } +} + +impl<'a> ClassReferenceChanger<'a, '_> { + // Check if the identifier reference is a reference to the class + fn is_class_reference(&self, ident: &IdentifierReference<'a>) -> bool { + self.ctx + .scoping() + .get_reference(ident.reference_id()) + .symbol_id() + .is_some_and(|symbol_id| self.class_binding.symbol_id == symbol_id) + } + + fn get_alias_ident_reference(&mut self) -> IdentifierReference<'a> { + let binding = self.class_alias_binding.get_or_insert_with(|| { + self.transformer_ctx.var_declarations.create_uid_var(&self.class_binding.name, self.ctx) + }); + + binding.create_read_reference(self.ctx) + } +} diff --git a/crates/swc_ecma_transformer/oxc/decorator/mod.rs b/crates/swc_ecma_transformer/oxc/decorator/mod.rs new file mode 100644 index 000000000000..eabd1732b7d8 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/decorator/mod.rs @@ -0,0 +1,156 @@ +mod legacy; +mod options; + +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +use legacy::LegacyDecorator; +pub use options::DecoratorOptions; + +pub struct Decorator<'a, 'ctx> { + options: DecoratorOptions, + + // Plugins + legacy_decorator: LegacyDecorator<'a, 'ctx>, +} + +impl<'a, 'ctx> Decorator<'a, 'ctx> { + pub fn new(options: DecoratorOptions, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + legacy_decorator: LegacyDecorator::new(options.emit_decorator_metadata, ctx), + options, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for Decorator<'a, '_> { + #[inline] + fn exit_program( + &mut self, + node: &mut Program<'a>, + ctx: &mut oxc_traverse::TraverseCtx<'a, TransformState<'a>>, + ) { + if self.options.legacy { + self.legacy_decorator.exit_program(node, ctx); + } + } + + #[inline] + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.legacy { + self.legacy_decorator.enter_statement(stmt, ctx); + } + } + + #[inline] + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.legacy { + self.legacy_decorator.exit_statement(stmt, ctx); + } + } + + #[inline] + fn enter_class(&mut self, node: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.legacy { + self.legacy_decorator.enter_class(node, ctx); + } + } + + #[inline] + fn exit_class(&mut self, node: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.legacy { + self.legacy_decorator.exit_class(node, ctx); + } + } + + #[inline] + fn enter_method_definition( + &mut self, + node: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.enter_method_definition(node, ctx); + } + } + + #[inline] + fn exit_method_definition( + &mut self, + node: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.exit_method_definition(node, ctx); + } + } + + #[inline] + fn enter_accessor_property( + &mut self, + node: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.enter_accessor_property(node, ctx); + } + } + + #[inline] + fn exit_accessor_property( + &mut self, + node: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.exit_accessor_property(node, ctx); + } + } + + #[inline] + fn enter_property_definition( + &mut self, + node: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.enter_property_definition(node, ctx); + } + } + + #[inline] + fn exit_property_definition( + &mut self, + node: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.exit_property_definition(node, ctx); + } + } + + #[inline] + fn enter_decorator( + &mut self, + node: &mut oxc_ast::ast::Decorator<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.enter_decorator(node, ctx); + } + } +} + +impl<'a> Decorator<'a, '_> { + #[inline] + pub fn exit_class_at_end(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.legacy { + self.legacy_decorator.exit_class_at_end(class, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/decorator/options.rs b/crates/swc_ecma_transformer/oxc/decorator/options.rs new file mode 100644 index 000000000000..81efcbac36d4 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/decorator/options.rs @@ -0,0 +1,20 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct DecoratorOptions { + /// Enables experimental support for decorators, which is a version of decorators that predates the TC39 standardization process. + /// + /// Decorators are a language feature which hasn’t yet been fully ratified into the JavaScript specification. + /// This means that the implementation version in TypeScript may differ from the implementation in JavaScript when it is decided by TC39. + /// + /// + #[serde(skip)] + pub legacy: bool, + + /// Enables emitting decorator metadata. + /// + /// This option is the same as [emitDecoratorMetadata](https://www.typescriptlang.org/tsconfig/#emitDecoratorMetadata) + /// in TypeScript, and it only works when `legacy` is true. + pub emit_decorator_metadata: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/es2015/arrow_functions.rs b/crates/swc_ecma_transformer/oxc/es2015/arrow_functions.rs new file mode 100644 index 000000000000..0c68ef2c858d --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2015/arrow_functions.rs @@ -0,0 +1,155 @@ +//! ES2015 Arrow Functions +//! +//! This plugin transforms arrow functions (`() => {}`) to function expressions (`function () {}`). +//! +//! > This plugin is included in `preset-env`, in ES2015 +//! +//! ## Missing features +//! +//! Implementation is incomplete at present. Still TODO: +//! +//! * `spec` option. +//! * Handle `arguments` in arrow functions. +//! * Handle `new.target` in arrow functions. +//! * Handle arrow function in function params (`function f(g = () => this) {}`). +//! Babel gets this wrong: +//! * Error on arrow functions in class properties. +//! +//! or we can support it: +//! `class C { x = () => this; }` +//! -> `class C { x = (function(_this) { return () => _this; })(this); }` +//! * Error on `super` in arrow functions. +//! +//! +//! ## Example +//! +//! Input: +//! ```js +//! var a = () => {}; +//! var a = b => b; +//! +//! const double = [1, 2, 3].map(num => num * 2); +//! console.log(double); // [2,4,6] +//! +//! var bob = { +//! name: "Bob", +//! friends: ["Sally", "Tom"], +//! printFriends() { +//! this.friends.forEach(f => console.log(this.name + " knows " + f)); +//! }, +//! }; +//! console.log(bob.printFriends()); +//! ``` +//! +//! Output: +//! ```js +//! var a = function() {}; +//! var a = function(b) { return b; }; +//! +//! const double = [1, 2, 3].map(function(num) { +//! return num * 2; +//! }); +//! console.log(double); // [2,4,6] +//! +//! var bob = { +//! name: "Bob", +//! friends: ["Sally", "Tom"], +//! printFriends() { +//! var _this = this; +//! this.friends.forEach(function(f) { +//! return console.log(_this.name + " knows " + f); +//! }); +//! }, +//! }; +//! console.log(bob.printFriends()); +//! ``` +//! +//! ## Options +//! +//! ### `spec` +//! +//! `boolean`, defaults to `false`. +//! +//! This option enables the following: +//! * Wrap the generated function in .bind(this) and keeps uses of this inside the function as-is, +//! instead of using a renamed this. +//! * Add a runtime check to ensure the functions are not instantiated. +//! * Add names to arrow functions. +//! +//! #### Example +//! +//! Using spec mode with the above example produces: +//! +//! ```js +//! var _this = this; +//! +//! var a = function a() { +//! babelHelpers.newArrowCheck(this, _this); +//! }.bind(this); +//! var a = function a(b) { +//! babelHelpers.newArrowCheck(this, _this); +//! return b; +//! }.bind(this); +//! +//! const double = [1, 2, 3].map( +//! function(num) { +//! babelHelpers.newArrowCheck(this, _this); +//! return num * 2; +//! }.bind(this) +//! ); +//! console.log(double); // [2,4,6] +//! +//! var bob = { +//! name: "Bob", +//! friends: ["Sally", "Tom"], +//! printFriends() { +//! var _this2 = this; +//! this.friends.forEach( +//! function(f) { +//! babelHelpers.newArrowCheck(this, _this2); +//! return console.log(this.name + " knows " + f); +//! }.bind(this) +//! ); +//! }, +//! }; +//! console.log(bob.printFriends()); +//! ``` +//! +//! ## Implementation +//! +//! The implementation is placed in [`crate::common::arrow_function_converter::ArrowFunctionConverter`], +//! which can be used in other plugins. +//! +//! ## References: +//! +//! * Babel plugin implementation: +//! * Arrow function specification: + +use serde::Deserialize; + +use oxc_traverse::Traverse; + +use crate::{context::TransformCtx, state::TransformState}; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +pub struct ArrowFunctionsOptions { + /// This option enables the following: + /// * Wrap the generated function in .bind(this) and keeps uses of this inside the function as-is, instead of using a renamed this. + /// * Add a runtime check to ensure the functions are not instantiated. + /// * Add names to arrow functions. + #[serde(default)] + pub spec: bool, +} + +pub struct ArrowFunctions<'a, 'ctx> { + _options: ArrowFunctionsOptions, + _ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> ArrowFunctions<'a, 'ctx> { + pub fn new(options: ArrowFunctionsOptions, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { _options: options, _ctx: ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ArrowFunctions<'a, '_> {} diff --git a/crates/swc_ecma_transformer/oxc/es2015/mod.rs b/crates/swc_ecma_transformer/oxc/es2015/mod.rs new file mode 100644 index 000000000000..414c85d342b1 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2015/mod.rs @@ -0,0 +1,29 @@ +use oxc_traverse::Traverse; + +use crate::{context::TransformCtx, state::TransformState}; + +mod arrow_functions; +mod options; + +pub use arrow_functions::{ArrowFunctions, ArrowFunctionsOptions}; +pub use options::ES2015Options; + +pub struct ES2015<'a, 'ctx> { + #[expect(unused)] + options: ES2015Options, + + // Plugins + #[expect(unused)] + arrow_functions: ArrowFunctions<'a, 'ctx>, +} + +impl<'a, 'ctx> ES2015<'a, 'ctx> { + pub fn new(options: ES2015Options, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + arrow_functions: ArrowFunctions::new(options.arrow_function.unwrap_or_default(), ctx), + options, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2015<'a, '_> {} diff --git a/crates/swc_ecma_transformer/oxc/es2015/options.rs b/crates/swc_ecma_transformer/oxc/es2015/options.rs new file mode 100644 index 000000000000..9201e92d9460 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2015/options.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +use super::ArrowFunctionsOptions; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2015Options { + #[serde(skip)] + pub arrow_function: Option, +} diff --git a/crates/swc_ecma_transformer/oxc/es2016/exponentiation_operator.rs b/crates/swc_ecma_transformer/oxc/es2016/exponentiation_operator.rs new file mode 100644 index 000000000000..82f083c7e9f3 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2016/exponentiation_operator.rs @@ -0,0 +1,572 @@ +//! ES2016: Exponentiation Operator +//! +//! This plugin transforms the exponentiation operator (`**`) to `Math.pow`. +//! +//! > This plugin is included in `preset-env`, in ES2016 +//! +//! ## Example +//! +//! Input: +//! ```js +//! let x = 10 ** 2; +//! x **= 3; +//! obj.prop **= 4; +//! ``` +//! +//! Output: +//! ```js +//! let x = Math.pow(10, 2); +//! x = Math.pow(x, 3); +//! obj["prop"] = Math.pow(obj["prop"], 4); +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-exponentiation-operator](https://babel.dev/docs/babel-plugin-transform-exponentiation-operator). +//! +//! ## References: +//! +//! * Babel plugin implementation: +//! +//! +//! * Exponentiation operator TC39 proposal: +//! * Exponentiation operator specification: + +use oxc_allocator::{CloneIn, TakeIn, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_semantic::ReferenceFlags; +use oxc_span::SPAN; +use oxc_syntax::operator::{AssignmentOperator, BinaryOperator}; +use oxc_traverse::{BoundIdentifier, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct ExponentiationOperator<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> ExponentiationOperator<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ExponentiationOperator<'a, '_> { + // Note: Do not transform to `Math.pow` with BigInt arguments - that's a runtime error + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + match expr { + // `left ** right` + Expression::BinaryExpression(binary_expr) => { + if binary_expr.operator != BinaryOperator::Exponential + || binary_expr.left.is_big_int_literal() + || binary_expr.right.is_big_int_literal() + { + return; + } + + Self::convert_binary_expression(expr, ctx); + } + // `left **= right` + Expression::AssignmentExpression(assign_expr) => { + if assign_expr.operator != AssignmentOperator::Exponential + || assign_expr.right.is_big_int_literal() + { + return; + } + + match &assign_expr.left { + AssignmentTarget::AssignmentTargetIdentifier(_) => { + self.convert_identifier_assignment(expr, ctx); + } + AssignmentTarget::StaticMemberExpression(_) => { + self.convert_static_member_expression_assignment(expr, ctx); + } + AssignmentTarget::ComputedMemberExpression(_) => { + self.convert_computed_member_expression_assignment(expr, ctx); + } + // Babel refuses to transform this: "We can't generate property ref for private name, + // please install `@babel/plugin-transform-class-properties`". + // But there's no reason not to. + AssignmentTarget::PrivateFieldExpression(_) => { + self.convert_private_field_assignment(expr, ctx); + } + _ => {} + } + } + _ => {} + } + } +} + +impl<'a> ExponentiationOperator<'a, '_> { + /// Convert `BinaryExpression`. + /// + /// `left ** right` -> `Math.pow(left, right)` + // + // `#[inline]` so compiler knows `expr` is a `BinaryExpression` + #[inline] + fn convert_binary_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let binary_expr = match expr.take_in(ctx.ast) { + Expression::BinaryExpression(binary_expr) => binary_expr.unbox(), + _ => unreachable!(), + }; + *expr = Self::math_pow(binary_expr.left, binary_expr.right, ctx); + } + + /// Convert `AssignmentExpression` where assignee is an identifier. + /// + /// `left **= right` transformed to: + /// * If `left` is a bound symbol: + /// -> `left = Math.pow(left, right)` + /// * If `left` is unbound: + /// -> `var _left; _left = left, left = Math.pow(_left, right)` + /// + /// Temporary variable `_left` is to avoid side-effects of getting `left` from running twice. + // + // `#[inline]` so compiler knows `expr` is an `AssignmentExpression` with `IdentifierReference` on left + #[inline] + fn convert_identifier_assignment(&self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + let AssignmentTarget::AssignmentTargetIdentifier(ident) = &mut assign_expr.left else { + unreachable!() + }; + + let (pow_left, temp_var_inits) = self.get_pow_left_identifier(ident, ctx); + Self::convert_assignment(assign_expr, pow_left, ctx); + Self::revise_expression(expr, temp_var_inits, ctx); + } + + /// Get left side of `Math.pow(pow_left, ...)` for identifier + fn get_pow_left_identifier( + &self, + ident: &IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ( + // Left side of `Math.pow(pow_left, ...)` + Expression<'a>, + // Temporary var initializations + ArenaVec<'a, Expression<'a>>, + ) { + let mut temp_var_inits = ctx.ast.vec(); + + // Make sure side-effects of evaluating `left` only happen once + let reference = ctx.scoping.scoping_mut().get_reference_mut(ident.reference_id()); + + // `left **= right` is being transformed to `left = Math.pow(left, right)`, + // so `left` in `left =` is no longer being read from + *reference.flags_mut() = ReferenceFlags::Write; + + let pow_left = if let Some(symbol_id) = reference.symbol_id() { + // This variable is declared in scope so evaluating it multiple times can't trigger a getter. + // No need for a temp var. + ctx.create_bound_ident_expr(SPAN, ident.name, symbol_id, ReferenceFlags::Read) + } else { + // Unbound reference. Could possibly trigger a getter so we need to only evaluate it once. + // Assign to a temp var. + let reference = ctx.create_unbound_ident_expr(SPAN, ident.name, ReferenceFlags::Read); + let binding = self.create_temp_var(reference, &mut temp_var_inits, ctx); + binding.create_read_expression(ctx) + }; + + (pow_left, temp_var_inits) + } + + /// Convert `AssignmentExpression` where assignee is a static member expression. + /// + /// `obj.prop **= right` transformed to: + /// * If `obj` is a bound symbol: + /// -> `obj["prop"] = Math.pow(obj["prop"], right)` + /// * If `obj` is unbound: + /// -> `var _obj; _obj = obj, _obj["prop"] = Math.pow(_obj["prop"], right)` + /// + /// `obj.foo.bar.qux **= right` transformed to: + /// ```js + /// var _obj$foo$bar; + /// _obj$foo$bar = obj.foo.bar, _obj$foo$bar["qux"] = Math.pow(_obj$foo$bar["qux"], right) + /// ``` + /// + /// Temporary variables are to avoid side-effects of getting `obj` / `obj.foo.bar` being run twice. + /// + /// TODO(improve-on-babel): `obj.prop` does not need to be transformed to `obj["prop"]`. + // + // `#[inline]` so compiler knows `expr` is an `AssignmentExpression` with `StaticMemberExpression` on left + #[inline] + fn convert_static_member_expression_assignment( + &self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + let AssignmentTarget::StaticMemberExpression(member_expr) = &mut assign_expr.left else { + unreachable!() + }; + + let (replacement_left, pow_left, temp_var_inits) = + self.get_pow_left_static_member(member_expr, ctx); + assign_expr.left = replacement_left; + Self::convert_assignment(assign_expr, pow_left, ctx); + Self::revise_expression(expr, temp_var_inits, ctx); + } + + /// Get left side of `Math.pow(pow_left, ...)` for static member expression + /// and replacement for left side of assignment. + fn get_pow_left_static_member( + &self, + member_expr: &mut StaticMemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ( + // Replacement left of assignment + AssignmentTarget<'a>, + // Left side of `Math.pow(pow_left, ...)` + Expression<'a>, + // Temporary var initializations + ArenaVec<'a, Expression<'a>>, + ) { + // Object part of 2nd member expression + // ``` + // obj["prop"] = Math.pow(obj["prop"], right) + // ^^^ + // ``` + let mut temp_var_inits = ctx.ast.vec(); + let obj = self.get_second_member_expression_object( + &mut member_expr.object, + &mut temp_var_inits, + ctx, + ); + + // Property part of 2nd member expression + // ``` + // obj["prop"] = Math.pow(obj["prop"], right) + // ^^^^^^ + // ``` + let prop_span = member_expr.property.span; + let prop_name = member_expr.property.name; + let prop = ctx.ast.expression_string_literal(prop_span, prop_name, None); + + // Complete 2nd member expression + // ``` + // obj["prop"] = Math.pow(obj["prop"], right) + // ^^^^^^^^^^^ + // ``` + let pow_left = Expression::from(ctx.ast.member_expression_computed(SPAN, obj, prop, false)); + + // Replacement for original member expression + // ``` + // obj["prop"] = Math.pow(obj["prop"], right) + // ^^^^^^^^^^^ + // ``` + let replacement_left = + AssignmentTarget::ComputedMemberExpression(ctx.ast.alloc_computed_member_expression( + member_expr.span, + member_expr.object.take_in(ctx.ast), + ctx.ast.expression_string_literal(prop_span, prop_name, None), + false, + )); + + (replacement_left, pow_left, temp_var_inits) + } + + /// Convert `AssignmentExpression` where assignee is a computed member expression. + /// + /// `obj[prop] **= right` transformed to: + /// * If `obj` is a bound symbol: + /// -> `var _prop; _prop = prop, obj[_prop] = Math.pow(obj[_prop], 2)` + /// * If `obj` is unbound: + /// -> `var _obj, _prop; _obj = obj, _prop = prop, _obj[_prop] = Math.pow(_obj[_prop], 2)` + /// + /// `obj.foo.bar[qux] **= right` transformed to: + /// ```js + /// var _obj$foo$bar, _qux; + /// _obj$foo$bar = obj.foo.bar, _qux = qux, _obj$foo$bar[_qux] = Math.pow(_obj$foo$bar[_qux], right) + /// ``` + /// + /// Temporary variables are to avoid side-effects of getting `obj` / `obj.foo.bar` or `prop` being run twice. + /// + /// TODO(improve-on-babel): + /// 1. If `prop` is bound, it doesn't need a temp variable `_prop`. + /// 2. Temp var initializations could be inlined: + /// * Current: `(_obj = obj, _prop = prop, _obj[_prop] = Math.pow(_obj[_prop], 2))` + /// * Could be: `(_obj = obj)[_prop = prop] = Math.pow(_obj[_prop], 2)` + // + // `#[inline]` so compiler knows `expr` is an `AssignmentExpression` with `ComputedMemberExpression` on left + #[inline] + fn convert_computed_member_expression_assignment( + &self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + let AssignmentTarget::ComputedMemberExpression(member_expr) = &mut assign_expr.left else { + unreachable!() + }; + + let (pow_left, temp_var_inits) = self.get_pow_left_computed_member(member_expr, ctx); + Self::convert_assignment(assign_expr, pow_left, ctx); + Self::revise_expression(expr, temp_var_inits, ctx); + } + + /// Get left side of `Math.pow(pow_left, ...)` for computed member expression + fn get_pow_left_computed_member( + &self, + member_expr: &mut ComputedMemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ( + // Left side of `Math.pow(pow_left, ...)` + Expression<'a>, + // Temporary var initializations + ArenaVec<'a, Expression<'a>>, + ) { + // Object part of 2nd member expression + // ``` + // obj[_prop] = Math.pow(obj[_prop], right) + // ^^^ + // ``` + let mut temp_var_inits = ctx.ast.vec(); + let obj = self.get_second_member_expression_object( + &mut member_expr.object, + &mut temp_var_inits, + ctx, + ); + + // Property part of 2nd member expression + // ``` + // obj[_prop] = Math.pow(obj[_prop], right) + // ^^^^^ replaced ^^^^^ prop + // ``` + let prop = &mut member_expr.expression; + let prop = if prop.is_literal() { + prop.clone_in(ctx.ast.allocator) + } else { + let owned_prop = prop.take_in(ctx.ast); + let binding = self.create_temp_var(owned_prop, &mut temp_var_inits, ctx); + *prop = binding.create_read_expression(ctx); + binding.create_read_expression(ctx) + }; + + // Complete 2nd member expression + // ``` + // obj[_prop] = Math.pow(obj[_prop], right) + // ^^^^^^^^^^ + // ``` + let pow_left = Expression::from(ctx.ast.member_expression_computed(SPAN, obj, prop, false)); + + (pow_left, temp_var_inits) + } + + /// Convert `AssignmentExpression` where assignee is a private field member expression. + /// + /// `obj.#prop **= right` transformed to: + /// * If `obj` is a bound symbol: + /// -> `obj.#prop = Math.pow(obj.#prop, right)` + /// * If `obj` is unbound: + /// -> `var _obj; _obj = obj, _obj.#prop = Math.pow(_obj.#prop, right)` + /// + /// `obj.foo.bar.#qux **= right` transformed to: + /// ```js + /// var _obj$foo$bar; + /// _obj$foo$bar = obj.foo.bar, _obj$foo$bar.#qux = Math.pow(_obj$foo$bar.#qux, right) + /// ``` + /// + /// Temporary variable is to avoid side-effects of getting `obj` / `obj.foo.bar` being run twice. + // + // `#[inline]` so compiler knows `expr` is an `AssignmentExpression` with `PrivateFieldExpression` on left + #[inline] + fn convert_private_field_assignment( + &self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + let AssignmentTarget::PrivateFieldExpression(member_expr) = &mut assign_expr.left else { + unreachable!() + }; + + let (pow_left, temp_var_inits) = self.get_pow_left_private_field(member_expr, ctx); + Self::convert_assignment(assign_expr, pow_left, ctx); + Self::revise_expression(expr, temp_var_inits, ctx); + } + + /// Get left side of `Math.pow(pow_left, ...)` for static member expression + /// and replacement for left side of assignment. + fn get_pow_left_private_field( + &self, + field_expr: &mut PrivateFieldExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ( + // Left side of `Math.pow(pow_left, ...)` + Expression<'a>, + // Temporary var initializations + ArenaVec<'a, Expression<'a>>, + ) { + // Object part of 2nd member expression + // ``` + // obj.#prop = Math.pow(obj.#prop, right) + // ^^^ + // ``` + let mut temp_var_inits = ctx.ast.vec(); + let obj = self.get_second_member_expression_object( + &mut field_expr.object, + &mut temp_var_inits, + ctx, + ); + + // Property part of 2nd member expression + // ``` + // obj.#prop = Math.pow(obj.#prop, right) + // ^^^^^ + // ``` + let field = field_expr.field.clone(); + + // Complete 2nd member expression + // ``` + // obj.#prop = Math.pow(obj.#prop, right) + // ^^^^^^^^^ + // ``` + let pow_left = Expression::from( + ctx.ast.member_expression_private_field_expression(SPAN, obj, field, false), + ); + + (pow_left, temp_var_inits) + } + + /// Get object part of 2nd member expression to be used as `left` in `Math.pow(left, right)`. + /// + /// Also update the original `obj` passed in to function, and add a temp var initializer, if necessary. + /// + /// Original: + /// ```js + /// obj.prop **= 2` + /// ^^^ original `obj` passed in to this function + /// ``` + /// + /// is transformed to: + /// + /// If `obj` is a bound symbol: + /// ```js + /// obj["prop"] = Math.pow(obj["prop"], 2) + /// ^^^ not updated ^^^ returned + /// ``` + /// + /// If `obj` is unbound: + /// ```js + /// var _obj; + /// _obj = obj, _obj["prop"] = Math.pow(_obj["prop"], 2) + /// ^^^^ updated ^^^^ returned + /// ^^^^^^^^^^ added to `temp_var_inits` + /// ``` + /// + /// Original: + /// ```js + /// obj.foo.bar.qux **= 2 + /// ^^^^^^^^^^^ original `obj` passed in to this function + /// ``` + /// is transformed to: + /// ```js + /// var _obj$foo$bar; + /// _obj$foo$bar = obj.foo.bar, _obj$foo$bar["qux"] = Math.pow(_obj$foo$bar["qux"], 2) + /// ^^^^^^^^^^^^ updated ^^^^^^^^^^^^ returned + /// ^^^^^^^^^^^^^^^^^^^^^^^^^^ added to `temp_var_inits` + /// ``` + fn get_second_member_expression_object( + &self, + obj: &mut Expression<'a>, + temp_var_inits: &mut ArenaVec<'a, Expression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // If the object reference that we need to save is locally declared, evaluating it multiple times + // will not trigger getters or setters. `super` cannot be directly assigned, so use it directly too. + // TODO(improve-on-babel): We could also skip creating a temp var for `this.x **= 2`. + match obj { + Expression::Super(super_) => return ctx.ast.expression_super(super_.span), + Expression::Identifier(ident) => { + let symbol_id = ctx.scoping().get_reference(ident.reference_id()).symbol_id(); + if let Some(symbol_id) = symbol_id { + // This variable is declared in scope so evaluating it multiple times can't trigger a getter. + // No need for a temp var. + return ctx.create_bound_ident_expr( + SPAN, + ident.name, + symbol_id, + ReferenceFlags::Read, + ); + } + // Unbound reference. Could possibly trigger a getter so we need to only evaluate it once. + // Assign to a temp var. + } + _ => { + // Other expression. Assign to a temp var. + } + } + + let binding = self.create_temp_var(obj.take_in(ctx.ast), temp_var_inits, ctx); + *obj = binding.create_read_expression(ctx); + binding.create_read_expression(ctx) + } + + /// `x **= right` -> `x = Math.pow(pow_left, right)` (with provided `pow_left`) + fn convert_assignment( + assign_expr: &mut AssignmentExpression<'a>, + pow_left: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let pow_right = assign_expr.right.take_in(ctx.ast); + assign_expr.right = Self::math_pow(pow_left, pow_right, ctx); + assign_expr.operator = AssignmentOperator::Assign; + } + + /// If needs temp var initializers, replace expression `expr` with `(temp1, temp2, expr)`. + fn revise_expression( + expr: &mut Expression<'a>, + mut temp_var_inits: ArenaVec<'a, Expression<'a>>, + ctx: &TraverseCtx<'a>, + ) { + if !temp_var_inits.is_empty() { + temp_var_inits.reserve_exact(1); + temp_var_inits.push(expr.take_in(ctx.ast)); + *expr = ctx.ast.expression_sequence(SPAN, temp_var_inits); + } + } + + /// `Math.pow(left, right)` + fn math_pow( + left: Expression<'a>, + right: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let math_symbol_id = ctx.scoping().find_binding(ctx.current_scope_id(), "Math"); + let object = + ctx.create_ident_expr(SPAN, Atom::from("Math"), math_symbol_id, ReferenceFlags::Read); + let property = ctx.ast.identifier_name(SPAN, "pow"); + let callee = + Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)); + let arguments = ctx.ast.vec_from_array([Argument::from(left), Argument::from(right)]); + ctx.ast.expression_call(SPAN, callee, NONE, arguments, false) + } + + /// Create a temporary variable. + /// Add a `var _name;` statement to enclosing scope. + /// Add initialization expression `_name = expr` to `temp_var_inits`. + /// Return `BoundIdentifier` for the temp var. + fn create_temp_var( + &self, + expr: Expression<'a>, + temp_var_inits: &mut ArenaVec<'a, Expression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + // var _name; + let binding = self.ctx.var_declarations.create_uid_var_based_on_node(&expr, ctx); + + // Add new reference `_name = name` to `temp_var_inits` + temp_var_inits.push(ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + binding.create_write_target(ctx), + expr, + )); + + binding + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2016/mod.rs b/crates/swc_ecma_transformer/oxc/es2016/mod.rs new file mode 100644 index 000000000000..6dedd9c36346 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2016/mod.rs @@ -0,0 +1,34 @@ +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +mod exponentiation_operator; +mod options; + +pub use exponentiation_operator::ExponentiationOperator; +pub use options::ES2016Options; + +pub struct ES2016<'a, 'ctx> { + options: ES2016Options, + + // Plugins + exponentiation_operator: ExponentiationOperator<'a, 'ctx>, +} + +impl<'a, 'ctx> ES2016<'a, 'ctx> { + pub fn new(options: ES2016Options, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { exponentiation_operator: ExponentiationOperator::new(ctx), options } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2016<'a, '_> { + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.exponentiation_operator { + self.exponentiation_operator.enter_expression(expr, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2016/options.rs b/crates/swc_ecma_transformer/oxc/es2016/options.rs new file mode 100644 index 000000000000..6d330043c4d8 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2016/options.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2016Options { + #[serde(skip)] + pub exponentiation_operator: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/es2017/async_to_generator.rs b/crates/swc_ecma_transformer/oxc/es2017/async_to_generator.rs new file mode 100644 index 000000000000..ac638d80b60a --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2017/async_to_generator.rs @@ -0,0 +1,895 @@ +//! ES2017: Async / Await +//! +//! This plugin transforms async functions to generator functions +//! and wraps them with `asyncToGenerator` helper function. +//! +//! ## Example +//! +//! Input: +//! ```js +//! async function foo() { +//! await bar(); +//! } +//! const foo2 = async () => { +//! await bar(); +//! }; +//! async () => { +//! await bar(); +//! } +//! ``` +//! +//! Output: +//! ```js +//! function foo() { +//! return _foo.apply(this, arguments); +//! } +//! function _foo() { +//! _foo = babelHelpers.asyncToGenerator(function* () { +//! yield bar(); +//! }); +//! return _foo.apply(this, arguments); +//! } +//! const foo2 = function() { +//! var _ref = babelHelpers.asyncToGenerator(function* () { +//! yield bar(); +//! }); +//! return function foo2() { +//! return _ref.apply(this, arguments); +//! }; +//! }(); +//! babelHelpers.asyncToGenerator(function* () { +//! yield bar(); +//! }); +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-async-to-generator](https://babel.dev/docs/babel-plugin-transform-async-to-generator). +//! +//! Reference: +//! * Babel docs: +//! * Babel implementation: +//! * Async / Await TC39 proposal: + +use std::{borrow::Cow, mem}; + +use oxc_allocator::{Box as ArenaBox, StringBuilder as ArenaStringBuilder, TakeIn}; +use oxc_ast::{NONE, ast::*}; +use oxc_ast_visit::Visit; +use oxc_semantic::{ReferenceFlags, ScopeFlags, ScopeId, SymbolFlags}; +use oxc_span::{Atom, GetSpan, SPAN}; +use oxc_syntax::{ + identifier::{is_identifier_name, is_identifier_part, is_identifier_start}, + keyword::is_reserved_keyword, +}; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse}; + +use crate::{ + common::helper_loader::Helper, + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct AsyncToGenerator<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + executor: AsyncGeneratorExecutor<'a, 'ctx>, +} + +impl<'a, 'ctx> AsyncToGenerator<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx, executor: AsyncGeneratorExecutor::new(Helper::AsyncToGenerator, ctx) } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for AsyncToGenerator<'a, '_> { + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let new_expr = match expr { + Expression::AwaitExpression(await_expr) => { + Self::transform_await_expression(await_expr, ctx) + } + Expression::FunctionExpression(func) => { + if func.r#async && !func.generator && !func.is_typescript_syntax() { + Some(self.executor.transform_function_expression(func, ctx)) + } else { + None + } + } + Expression::ArrowFunctionExpression(arrow) => { + if arrow.r#async { + Some(self.executor.transform_arrow_function(arrow, ctx)) + } else { + None + } + } + _ => None, + }; + + if let Some(new_expr) = new_expr { + *expr = new_expr; + } + } + + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let function = match stmt { + Statement::FunctionDeclaration(func) => Some(func), + Statement::ExportDefaultDeclaration(decl) => { + if let ExportDefaultDeclarationKind::FunctionDeclaration(func) = + &mut decl.declaration + { + Some(func) + } else { + None + } + } + Statement::ExportNamedDeclaration(decl) => { + if let Some(Declaration::FunctionDeclaration(func)) = &mut decl.declaration { + Some(func) + } else { + None + } + } + _ => None, + }; + + if let Some(function) = function + && function.r#async + && !function.generator + && !function.is_typescript_syntax() + { + let new_statement = self.executor.transform_function_declaration(function, ctx); + self.ctx.statement_injector.insert_after(stmt, new_statement); + } + } + + fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if func.r#async + && !func.is_typescript_syntax() + && AsyncGeneratorExecutor::is_class_method_like_ancestor(ctx.parent()) + { + self.executor.transform_function_for_method_definition(func, ctx); + } + } +} + +impl<'a> AsyncToGenerator<'a, '_> { + /// Check whether the current node is inside an async function. + fn is_inside_async_function(ctx: &TraverseCtx<'a>) -> bool { + // Early return if current scope is top because we don't need to transform top-level await expression. + if ctx.current_scope_flags().is_top() { + return false; + } + + for ancestor in ctx.ancestors() { + match ancestor { + Ancestor::FunctionBody(func) => return *func.r#async(), + Ancestor::ArrowFunctionExpressionBody(func) => { + return *func.r#async(); + } + _ => {} + } + } + false + } + + /// Transforms `await` expressions to `yield` expressions. + /// Ignores top-level await expressions. + fn transform_await_expression( + expr: &mut AwaitExpression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Option> { + // We don't need to handle top-level await. + if Self::is_inside_async_function(ctx) { + Some(ctx.ast.expression_yield(SPAN, false, Some(expr.argument.take_in(ctx.ast)))) + } else { + None + } + } +} + +pub struct AsyncGeneratorExecutor<'a, 'ctx> { + helper: Helper, + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> AsyncGeneratorExecutor<'a, 'ctx> { + pub fn new(helper: Helper, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { helper, ctx } + } + + /// Transforms async method definitions to generator functions wrapped in asyncToGenerator. + /// + /// ## Example + /// + /// Input: + /// ```js + /// class A { async foo() { await bar(); } } + /// ``` + /// + /// Output: + /// ```js + /// class A { + /// foo() { + /// return babelHelpers.asyncToGenerator(function* () { + /// yield bar(); + /// })(); + /// } + /// ``` + pub fn transform_function_for_method_definition( + &self, + func: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Some(body) = func.body.take() else { + return; + }; + + // If parameters could throw errors, we need to move them to the inner function, + // because it is an async function, which should return a rejecting promise if + // there is an error. + let needs_move_parameters_to_inner_function = + Self::could_throw_errors_parameters(&func.params); + + let (generator_scope_id, wrapper_scope_id) = { + let new_scope_id = ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + let scope_id = func.scope_id.replace(Some(new_scope_id)).unwrap(); + // We need to change the parent id to new scope id because we need to this function's body inside the wrapper function, + // and then the new scope id will be wrapper function's scope id. + ctx.scoping_mut().change_scope_parent_id(scope_id, Some(new_scope_id)); + if !needs_move_parameters_to_inner_function { + // We need to change formal parameters's scope back to the original scope, + // because we only move out the function body. + Self::move_formal_parameters_to_target_scope(new_scope_id, &func.params, ctx); + } + + (scope_id, new_scope_id) + }; + + let params = if needs_move_parameters_to_inner_function { + // Make sure to not change the value of the "length" property. This is + // done by generating dummy arguments for the outer function equal to + // the expected length of the function: + // + // async function foo(a, b, c = d, ...e) { + // } + // + // This turns into: + // + // function foo(_x, _x1) { + // return _asyncToGenerator(function* (a, b, c = d, ...e) { + // }).call(this, arguments); + // } + // + // The "_x" and "_x1" are dummy variables to ensure "foo.length" is 2. + let new_params = Self::create_placeholder_params(&func.params, wrapper_scope_id, ctx); + mem::replace(&mut func.params, new_params) + } else { + Self::create_empty_params(ctx) + }; + + let callee = self.create_async_to_generator_call(params, body, generator_scope_id, ctx); + let (callee, arguments) = if needs_move_parameters_to_inner_function { + // callee.apply(this, arguments) + let property = ctx.ast.identifier_name(SPAN, "apply"); + let callee = + Expression::from(ctx.ast.member_expression_static(SPAN, callee, property, false)); + + // this, arguments + let this_argument = Argument::from(ctx.ast.expression_this(SPAN)); + let arguments_argument = Argument::from(ctx.create_unbound_ident_expr( + SPAN, + Atom::new_const("arguments"), + ReferenceFlags::Read, + )); + (callee, ctx.ast.vec_from_array([this_argument, arguments_argument])) + } else { + // callee() + (callee, ctx.ast.vec()) + }; + + let expression = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false); + let statement = ctx.ast.statement_return(SPAN, Some(expression)); + + // Modify the wrapper function + func.r#async = false; + func.generator = false; + func.body = Some(ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), ctx.ast.vec1(statement))); + func.scope_id.set(Some(wrapper_scope_id)); + } + + /// Transforms [`Function`] whose type is [`FunctionType::FunctionExpression`] to a generator function + /// and wraps it in asyncToGenerator helper function. + pub fn transform_function_expression( + &self, + wrapper_function: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let body = wrapper_function.body.take().unwrap(); + let params = wrapper_function.params.take_in_box(ctx.ast); + let id = wrapper_function.id.take(); + let has_function_id = id.is_some(); + + if !has_function_id && !Self::is_function_length_affected(¶ms) { + return self.create_async_to_generator_call( + params, + body, + wrapper_function.scope_id.take().unwrap(), + ctx, + ); + } + + let (generator_scope_id, wrapper_scope_id) = { + let wrapper_scope_id = + ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + let scope_id = wrapper_function.scope_id.replace(Some(wrapper_scope_id)).unwrap(); + // Change the parent scope of the function scope with the current scope. + ctx.scoping_mut().change_scope_parent_id(scope_id, Some(wrapper_scope_id)); + // If there is an id, then we will use it as the name of caller_function, + // and the caller_function is inside the wrapper function. + // so we need to move the id to the new scope. + if let Some(id) = id.as_ref() { + Self::move_binding_identifier_to_target_scope(wrapper_scope_id, id, ctx); + let symbol_id = id.symbol_id(); + *ctx.scoping_mut().symbol_flags_mut(symbol_id) = SymbolFlags::Function; + } + (scope_id, wrapper_scope_id) + }; + + let bound_ident = Self::create_bound_identifier( + id.as_ref(), + wrapper_scope_id, + SymbolFlags::FunctionScopedVariable, + ctx, + ); + + let caller_function = { + let scope_id = ctx.create_child_scope(wrapper_scope_id, ScopeFlags::Function); + let params = Self::create_placeholder_params(¶ms, scope_id, ctx); + let statements = ctx.ast.vec1(Self::create_apply_call_statement(&bound_ident, ctx)); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); + let id = id.or_else(|| Self::infer_function_id_from_parent_node(wrapper_scope_id, ctx)); + Self::create_function(id, params, body, scope_id, ctx) + }; + + { + // Modify the wrapper function to add new body, params, and scope_id. + let async_to_gen_decl = self.create_async_to_generator_declaration( + &bound_ident, + params, + body, + generator_scope_id, + ctx, + ); + let statements = if has_function_id { + let id = caller_function.id.as_ref().unwrap(); + // If the function has an id, then we need to return the id. + // `function foo() { ... }` -> `function foo() {} return foo;` + let reference = ctx.create_bound_ident_expr( + SPAN, + id.name, + id.symbol_id(), + ReferenceFlags::Read, + ); + let func_decl = Statement::FunctionDeclaration(caller_function); + let statement_return = ctx.ast.statement_return(SPAN, Some(reference)); + ctx.ast.vec_from_array([async_to_gen_decl, func_decl, statement_return]) + } else { + // If the function doesn't have an id, then we need to return the function itself. + // `function() { ... }` -> `return function() { ... };` + let statement_return = ctx + .ast + .statement_return(SPAN, Some(Expression::FunctionExpression(caller_function))); + ctx.ast.vec_from_array([async_to_gen_decl, statement_return]) + }; + debug_assert!(wrapper_function.body.is_none()); + wrapper_function.r#async = false; + wrapper_function.generator = false; + wrapper_function.body.replace(ctx.ast.alloc_function_body( + SPAN, + ctx.ast.vec(), + statements, + )); + } + + // Construct the IIFE + let callee = Expression::FunctionExpression(wrapper_function.take_in_box(ctx.ast)); + ctx.ast.expression_call_with_pure(SPAN, callee, NONE, ctx.ast.vec(), false, true) + } + + /// Transforms async function declarations into generator functions wrapped in the asyncToGenerator helper. + pub fn transform_function_declaration( + &self, + wrapper_function: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let (generator_scope_id, wrapper_scope_id) = { + let wrapper_scope_id = + ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + let scope_id = wrapper_function.scope_id.replace(Some(wrapper_scope_id)).unwrap(); + // Change the parent scope of the function scope with the current scope. + ctx.scoping_mut().change_scope_parent_id(scope_id, Some(wrapper_scope_id)); + (scope_id, wrapper_scope_id) + }; + let body = wrapper_function.body.take().unwrap(); + let params = + Self::create_placeholder_params(&wrapper_function.params, wrapper_scope_id, ctx); + let params = mem::replace(&mut wrapper_function.params, params); + + let bound_ident = Self::create_bound_identifier( + wrapper_function.id.as_ref(), + ctx.current_scope_id(), + SymbolFlags::Function, + ctx, + ); + + // Modify the wrapper function + { + wrapper_function.r#async = false; + wrapper_function.generator = false; + let statements = ctx.ast.vec1(Self::create_apply_call_statement(&bound_ident, ctx)); + debug_assert!(wrapper_function.body.is_none()); + wrapper_function.body.replace(ctx.ast.alloc_function_body( + SPAN, + ctx.ast.vec(), + statements, + )); + } + + // function _name() { _ref.apply(this, arguments); } + { + let statements = ctx.ast.vec_from_array([ + self.create_async_to_generator_assignment( + &bound_ident, + params, + body, + generator_scope_id, + ctx, + ), + Self::create_apply_call_statement(&bound_ident, ctx), + ]); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); + + let scope_id = ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + // The generator function will move to this function, so we need + // to change the parent scope of the generator function to the scope of this function. + ctx.scoping_mut().change_scope_parent_id(generator_scope_id, Some(scope_id)); + + let params = Self::create_empty_params(ctx); + let id = Some(bound_ident.create_binding_identifier(ctx)); + let caller_function = Self::create_function(id, params, body, scope_id, ctx); + Statement::FunctionDeclaration(caller_function) + } + } + + /// Transforms async arrow functions into generator functions wrapped in the asyncToGenerator helper. + pub(self) fn transform_arrow_function( + &self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let mut body = arrow.body.take_in_box(ctx.ast); + + // If the arrow's expression is true, we need to wrap the only one expression with return statement. + if arrow.expression { + let statement = body.statements.first_mut().unwrap(); + let expression = match statement { + Statement::ExpressionStatement(es) => es.expression.take_in(ctx.ast), + _ => unreachable!(), + }; + *statement = ctx.ast.statement_return(expression.span(), Some(expression)); + } + + let params = arrow.params.take_in_box(ctx.ast); + let generator_function_id = arrow.scope_id(); + ctx.scoping_mut().scope_flags_mut(generator_function_id).remove(ScopeFlags::Arrow); + let function_name = Self::infer_function_name_from_parent_node(ctx); + + if function_name.is_none() && !Self::is_function_length_affected(¶ms) { + return self.create_async_to_generator_call(params, body, generator_function_id, ctx); + } + + let wrapper_scope_id = ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + + // The generator function will move to inside wrapper, so we need + // to change the parent scope of the generator function to the wrapper function. + ctx.scoping_mut().change_scope_parent_id(generator_function_id, Some(wrapper_scope_id)); + + let bound_ident = Self::create_bound_identifier( + None, + wrapper_scope_id, + SymbolFlags::FunctionScopedVariable, + ctx, + ); + + let caller_function = { + let scope_id = ctx.create_child_scope(wrapper_scope_id, ScopeFlags::Function); + let params = Self::create_placeholder_params(¶ms, scope_id, ctx); + let statements = ctx.ast.vec1(Self::create_apply_call_statement(&bound_ident, ctx)); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); + let id = function_name.map(|name| { + ctx.generate_binding(name, wrapper_scope_id, SymbolFlags::Function) + .create_binding_identifier(ctx) + }); + let function = Self::create_function(id, params, body, scope_id, ctx); + let argument = Some(Expression::FunctionExpression(function)); + ctx.ast.statement_return(SPAN, argument) + }; + + // Wrapper function + { + let statement = self.create_async_to_generator_declaration( + &bound_ident, + params, + body, + generator_function_id, + ctx, + ); + let statements = ctx.ast.vec_from_array([statement, caller_function]); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); + let params = Self::create_empty_params(ctx); + let wrapper_function = Self::create_function(None, params, body, wrapper_scope_id, ctx); + // Construct the IIFE + let callee = Expression::FunctionExpression(wrapper_function); + ctx.ast.expression_call(SPAN, callee, NONE, ctx.ast.vec(), false) + } + } + + /// Infers the function id from [`TraverseCtx::parent`]. + fn infer_function_id_from_parent_node( + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let name = Self::infer_function_name_from_parent_node(ctx)?; + Some( + ctx.generate_binding(name, scope_id, SymbolFlags::Function) + .create_binding_identifier(ctx), + ) + } + + /// Infers the function name from the [`TraverseCtx::parent`]. + fn infer_function_name_from_parent_node(ctx: &TraverseCtx<'a>) -> Option> { + match ctx.parent() { + // infer `foo` from `const foo = async function() {}` + Ancestor::VariableDeclaratorInit(declarator) => { + declarator.id().get_binding_identifier().map(|id| id.name) + } + // infer `foo` from `({ foo: async function() {} })` + Ancestor::ObjectPropertyValue(property) if !*property.method() => { + property.key().static_name().map(|key| Self::normalize_function_name(&key, ctx)) + } + _ => None, + } + } + + /// Normalizes the function name. + /// + /// Examples: + /// + /// // Valid + /// * `foo` -> `foo` + /// // Contains space + /// * `foo bar` -> `foo_bar` + /// // Reserved keyword + /// * `this` -> `_this` + /// * `arguments` -> `_arguments` + fn normalize_function_name(input: &Cow<'a, str>, ctx: &TraverseCtx<'a>) -> Atom<'a> { + let input_str = input.as_ref(); + if !is_reserved_keyword(input_str) && is_identifier_name(input_str) { + return ctx.ast.atom_from_cow(input); + } + + let mut name = ArenaStringBuilder::with_capacity_in(input_str.len() + 1, ctx.ast.allocator); + let mut capitalize_next = false; + + let mut chars = input_str.chars(); + if let Some(first) = chars.next() + && is_identifier_start(first) + { + name.push(first); + } + + for c in chars { + if c == ' ' { + name.push('_'); + } else if !is_identifier_part(c) { + capitalize_next = true; + } else if capitalize_next { + name.push(c.to_ascii_uppercase()); + capitalize_next = false; + } else { + name.push(c); + } + } + + if name.is_empty() { + return Atom::from("_"); + } + + if is_reserved_keyword(name.as_str()) { + name.push_ascii_byte_start(b'_'); + } + + Atom::from(name) + } + + /// Creates a [`Function`] with the specified params, body and scope_id. + #[inline] + fn create_function( + id: Option>, + params: ArenaBox<'a, FormalParameters<'a>>, + body: ArenaBox<'a, FunctionBody<'a>>, + scope_id: ScopeId, + ctx: &TraverseCtx<'a>, + ) -> ArenaBox<'a, Function<'a>> { + let r#type = if id.is_some() { + FunctionType::FunctionDeclaration + } else { + FunctionType::FunctionExpression + }; + ctx.ast.alloc_function_with_scope_id( + SPAN, + r#type, + id, + false, + false, + false, + NONE, + NONE, + params, + NONE, + Some(body), + scope_id, + ) + } + + /// Creates a [`Statement`] that calls the `apply` method on the bound identifier. + /// + /// The generated code structure is: + /// ```js + /// bound_ident.apply(this, arguments); + /// ``` + fn create_apply_call_statement( + bound_ident: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let symbol_id = ctx.scoping().find_binding(ctx.current_scope_id(), "arguments"); + let arguments_ident = Argument::from(ctx.create_ident_expr( + SPAN, + Atom::from("arguments"), + symbol_id, + ReferenceFlags::Read, + )); + + // (this, arguments) + let this = Argument::from(ctx.ast.expression_this(SPAN)); + let arguments = ctx.ast.vec_from_array([this, arguments_ident]); + // _ref.apply + let callee = Expression::from(ctx.ast.member_expression_static( + SPAN, + bound_ident.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, "apply"), + false, + )); + let argument = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false); + ctx.ast.statement_return(SPAN, Some(argument)) + } + + /// Creates an [`Expression`] that calls the [`AsyncGeneratorExecutor::helper`] helper function. + /// + /// This function constructs the helper call with arguments derived from the provided + /// parameters, body, and scope_id. + /// + /// The generated code structure is: + /// ```js + /// asyncToGenerator(function* (PARAMS) { + /// BODY + /// }); + /// ``` + fn create_async_to_generator_call( + &self, + params: ArenaBox<'a, FormalParameters<'a>>, + body: ArenaBox<'a, FunctionBody<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let mut function = Self::create_function(None, params, body, scope_id, ctx); + function.generator = true; + let arguments = ctx.ast.vec1(Argument::FunctionExpression(function)); + self.ctx.helper_call_expr(self.helper, SPAN, arguments, ctx) + } + + /// Creates a helper declaration statement for async-to-generator transformation. + /// + /// This function generates code that looks like: + /// ```js + /// var _ref = asyncToGenerator(function* (PARAMS) { + /// BODY + /// }); + /// ``` + fn create_async_to_generator_declaration( + &self, + bound_ident: &BoundIdentifier<'a>, + params: ArenaBox<'a, FormalParameters<'a>>, + body: ArenaBox<'a, FunctionBody<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let init = self.create_async_to_generator_call(params, body, scope_id, ctx); + let declarations = ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + bound_ident.create_binding_pattern(ctx), + Some(init), + false, + )); + Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + declarations, + false, + )) + } + + /// Creates a helper assignment statement for async-to-generator transformation. + /// + /// This function generates code that looks like: + /// ```js + /// _ref = asyncToGenerator(function* (PARAMS) { + /// BODY + /// }); + /// ``` + fn create_async_to_generator_assignment( + &self, + bound: &BoundIdentifier<'a>, + params: ArenaBox<'a, FormalParameters<'a>>, + body: ArenaBox<'a, FunctionBody<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let right = self.create_async_to_generator_call(params, body, scope_id, ctx); + let expression = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + bound.create_write_target(ctx), + right, + ); + ctx.ast.statement_expression(SPAN, expression) + } + + /// Creates placeholder [`FormalParameters`] which named `_x` based on the passed-in parameters. + /// `function p(x, y, z, d = 0, ...rest) {}` -> `function* (_x, _x1, _x2) {}` + fn create_placeholder_params( + params: &FormalParameters<'a>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> ArenaBox<'a, FormalParameters<'a>> { + let mut parameters = ctx.ast.vec_with_capacity(params.items.len()); + for param in ¶ms.items { + if param.pattern.kind.is_assignment_pattern() { + break; + } + let binding = ctx.generate_uid("x", scope_id, SymbolFlags::FunctionScopedVariable); + parameters.push( + ctx.ast.plain_formal_parameter(param.span(), binding.create_binding_pattern(ctx)), + ); + } + + ctx.ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + parameters, + NONE, + ) + } + + /// Creates an empty [FormalParameters] with [FormalParameterKind::FormalParameter]. + #[inline] + fn create_empty_params(ctx: &TraverseCtx<'a>) -> ArenaBox<'a, FormalParameters<'a>> { + ctx.ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + ctx.ast.vec(), + NONE, + ) + } + + /// Creates a [`BoundIdentifier`] for the id of the function. + #[inline] + fn create_bound_identifier( + id: Option<&BindingIdentifier<'a>>, + scope_id: ScopeId, + flags: SymbolFlags, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + ctx.generate_uid(id.as_ref().map_or_else(|| "ref", |id| id.name.as_str()), scope_id, flags) + } + + /// Check whether the given [`Ancestor`] is a class method-like node. + pub(crate) fn is_class_method_like_ancestor(ancestor: Ancestor) -> bool { + match ancestor { + // `class A { async foo() {} }` + Ancestor::MethodDefinitionValue(_) => true, + // Only `({ async foo() {} })` does not include non-method like `({ foo: async function() {} })`, + // because it's just a property with a function value + Ancestor::ObjectPropertyValue(property) => *property.method(), + _ => false, + } + } + + /// Checks if the function length is affected by the parameters. + /// + /// TODO: Needs to handle `ignoreFunctionLength` assumption. + // + #[inline] + fn is_function_length_affected(params: &FormalParameters<'_>) -> bool { + params.items.first().is_some_and(|param| !param.pattern.kind.is_assignment_pattern()) + } + + /// Check whether the function parameters could throw errors. + #[inline] + fn could_throw_errors_parameters(params: &FormalParameters<'a>) -> bool { + params.items.iter().any(|param| + matches!( + ¶m.pattern.kind, + BindingPatternKind::AssignmentPattern(pattern) if Self::could_potentially_throw_error_expression(&pattern.right) + ) + ) + } + + /// Check whether the expression could potentially throw an error. + #[inline] + fn could_potentially_throw_error_expression(expr: &Expression<'a>) -> bool { + !(matches!( + expr, + Expression::NullLiteral(_) + | Expression::BooleanLiteral(_) + | Expression::NumericLiteral(_) + | Expression::StringLiteral(_) + | Expression::BigIntLiteral(_) + | Expression::ArrowFunctionExpression(_) + | Expression::FunctionExpression(_) + ) || expr.is_undefined()) + } + + #[inline] + fn move_formal_parameters_to_target_scope( + target_scope_id: ScopeId, + params: &FormalParameters<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + BindingMover::new(target_scope_id, ctx).visit_formal_parameters(params); + } + + #[inline] + fn move_binding_identifier_to_target_scope( + target_scope_id: ScopeId, + ident: &BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + BindingMover::new(target_scope_id, ctx).visit_binding_identifier(ident); + } +} + +/// Moves the bindings from original scope to target scope. +struct BindingMover<'a, 'ctx> { + ctx: &'ctx mut TraverseCtx<'a>, + target_scope_id: ScopeId, +} + +impl<'a, 'ctx> BindingMover<'a, 'ctx> { + fn new(target_scope_id: ScopeId, ctx: &'ctx mut TraverseCtx<'a>) -> Self { + Self { ctx, target_scope_id } + } +} + +impl<'a> Visit<'a> for BindingMover<'a, '_> { + /// Visits a binding identifier and moves it to the target scope. + fn visit_binding_identifier(&mut self, ident: &BindingIdentifier<'a>) { + let symbols = self.ctx.scoping(); + let symbol_id = ident.symbol_id(); + let current_scope_id = symbols.symbol_scope_id(symbol_id); + let scopes = self.ctx.scoping_mut(); + scopes.move_binding(current_scope_id, self.target_scope_id, ident.name.as_str()); + let symbols = self.ctx.scoping_mut(); + symbols.set_symbol_scope_id(symbol_id, self.target_scope_id); + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2017/mod.rs b/crates/swc_ecma_transformer/oxc/es2017/mod.rs new file mode 100644 index 000000000000..c0c37acc77d5 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2017/mod.rs @@ -0,0 +1,45 @@ +use oxc_ast::ast::{Expression, Function, Statement}; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +mod async_to_generator; +mod options; +pub use async_to_generator::{AsyncGeneratorExecutor, AsyncToGenerator}; +pub use options::ES2017Options; + +pub struct ES2017<'a, 'ctx> { + options: ES2017Options, + + // Plugins + async_to_generator: AsyncToGenerator<'a, 'ctx>, +} + +impl<'a, 'ctx> ES2017<'a, 'ctx> { + pub fn new(options: ES2017Options, ctx: &'ctx TransformCtx<'a>) -> ES2017<'a, 'ctx> { + ES2017 { async_to_generator: AsyncToGenerator::new(ctx), options } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2017<'a, '_> { + fn exit_expression(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_to_generator { + self.async_to_generator.exit_expression(node, ctx); + } + } + + fn exit_function(&mut self, node: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_to_generator { + self.async_to_generator.exit_function(node, ctx); + } + } + + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_to_generator { + self.async_to_generator.exit_statement(stmt, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2017/options.rs b/crates/swc_ecma_transformer/oxc/es2017/options.rs new file mode 100644 index 000000000000..d907633862be --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2017/options.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2017Options { + #[serde(skip)] + pub async_to_generator: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/es2018/async_generator_functions/for_await.rs b/crates/swc_ecma_transformer/oxc/es2018/async_generator_functions/for_await.rs new file mode 100644 index 000000000000..19fc3705f46e --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2018/async_generator_functions/for_await.rs @@ -0,0 +1,480 @@ +//! This module is responsible for transforming `for await` to `for` statement + +use oxc_allocator::{TakeIn, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_semantic::{ScopeFlags, ScopeId, SymbolFlags}; +use oxc_span::SPAN; +use oxc_traverse::{Ancestor, BoundIdentifier}; + +use crate::{common::helper_loader::Helper, context::TraverseCtx}; + +use super::AsyncGeneratorFunctions; + +impl<'a> AsyncGeneratorFunctions<'a, '_> { + /// Check the parent node to see if multiple statements are allowed. + fn is_multiple_statements_allowed(ctx: &TraverseCtx<'a>) -> bool { + matches!( + ctx.parent(), + Ancestor::ProgramBody(_) + | Ancestor::FunctionBodyStatements(_) + | Ancestor::BlockStatementBody(_) + | Ancestor::SwitchCaseConsequent(_) + | Ancestor::StaticBlockBody(_) + | Ancestor::TSModuleBlockBody(_) + ) + } + + pub(crate) fn transform_statement(&self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let (for_of, label) = match stmt { + Statement::LabeledStatement(labeled) => { + let LabeledStatement { label, body, .. } = labeled.as_mut(); + if let Statement::ForOfStatement(for_of) = body { + (for_of, Some(label)) + } else { + return; + } + } + Statement::ForOfStatement(for_of) => (for_of, None), + _ => return, + }; + + if !for_of.r#await { + return; + } + + let allow_multiple_statements = Self::is_multiple_statements_allowed(ctx); + let parent_scope_id = if allow_multiple_statements { + ctx.current_scope_id() + } else { + ctx.create_child_scope_of_current(ScopeFlags::empty()) + }; + + // We need to replace the current statement with new statements, + // but we don't have a such method to do it, so we leverage the statement injector. + // + // Now, we use below steps to workaround it: + // 1. Use the last statement as the new statement. + // 2. insert the rest of the statements before the current statement. + // TODO: Once we have a method to replace the current statement, we can simplify this logic. + let mut statements = self.transform_for_of_statement(for_of, parent_scope_id, ctx); + let mut new_stmt = statements.pop().unwrap(); + + // If it's a labeled statement, we need to wrap the ForStatement with a labeled statement. + if let Some(label) = label { + let Statement::TryStatement(try_statement) = &mut new_stmt else { + unreachable!( + "The last statement should be a try statement, please see the `build_for_await` function" + ); + }; + let try_statement_block_body = &mut try_statement.block.body; + let for_statement = try_statement_block_body.pop().unwrap(); + try_statement_block_body.push(ctx.ast.statement_labeled( + SPAN, + label.clone(), + for_statement, + )); + } + self.ctx.statement_injector.insert_many_before(&new_stmt, statements); + + // If the parent node doesn't allow multiple statements, we need to wrap the new statement + // with a block statement, this way we can ensure can insert statement correctly. + // e.g. `if (true) statement` to `if (true) { statement }` + if !allow_multiple_statements { + new_stmt = ctx.ast.statement_block_with_scope_id( + SPAN, + ctx.ast.vec1(new_stmt), + parent_scope_id, + ); + } + *stmt = new_stmt; + } + + pub(self) fn transform_for_of_statement( + &self, + stmt: &mut ForOfStatement<'a>, + parent_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> ArenaVec<'a, Statement<'a>> { + let step_key = + ctx.generate_uid("step", ctx.current_scope_id(), SymbolFlags::FunctionScopedVariable); + // step.value + let step_value = Expression::from(ctx.ast.member_expression_static( + SPAN, + step_key.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, "value"), + false, + )); + + let assignment_statement = match &mut stmt.left { + ForStatementLeft::VariableDeclaration(variable) => { + // for await (let i of test) + let mut declarator = variable.declarations.pop().unwrap(); + declarator.init = Some(step_value); + Statement::VariableDeclaration(ctx.ast.alloc_variable_declaration( + SPAN, + declarator.kind, + ctx.ast.vec1(declarator), + false, + )) + } + left @ match_assignment_target!(ForStatementLeft) => { + // for await (i of test), for await ({ i } of test) + let target = left.to_assignment_target_mut().take_in(ctx.ast); + let expression = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + target, + step_value, + ); + ctx.ast.statement_expression(SPAN, expression) + } + }; + + let body = { + let mut statements = ctx.ast.vec_with_capacity(2); + statements.push(assignment_statement); + let stmt_body = &mut stmt.body; + if let Statement::BlockStatement(block) = stmt_body { + if block.body.is_empty() { + // If the block is empty, we don’t need to add it to the body; + // instead, we need to remove the useless scope. + ctx.scoping_mut().delete_scope(block.scope_id()); + } else { + statements.push(stmt_body.take_in(ctx.ast)); + } + } + statements + }; + + let iterator = stmt.right.take_in(ctx.ast); + let iterator = self.ctx.helper_call_expr( + Helper::AsyncIterator, + SPAN, + ctx.ast.vec1(Argument::from(iterator)), + ctx, + ); + Self::build_for_await(iterator, &step_key, body, stmt.scope_id(), parent_scope_id, ctx) + } + + /// Build a `for` statement used to replace the `for await` statement. + /// + /// This function builds the following code: + /// + /// ```js + // var ITERATOR_ABRUPT_COMPLETION = false; + // var ITERATOR_HAD_ERROR_KEY = false; + // var ITERATOR_ERROR_KEY; + // try { + // for ( + // var ITERATOR_KEY = GET_ITERATOR(OBJECT), STEP_KEY; + // ITERATOR_ABRUPT_COMPLETION = !(STEP_KEY = await ITERATOR_KEY.next()).done; + // ITERATOR_ABRUPT_COMPLETION = false + // ) { + // } + // } catch (err) { + // ITERATOR_HAD_ERROR_KEY = true; + // ITERATOR_ERROR_KEY = err; + // } finally { + // try { + // if (ITERATOR_ABRUPT_COMPLETION && ITERATOR_KEY.return != null) { + // await ITERATOR_KEY.return(); + // } + // } finally { + // if (ITERATOR_HAD_ERROR_KEY) { + // throw ITERATOR_ERROR_KEY; + // } + // } + // } + /// ``` + /// + /// Based on Babel's implementation: + /// + fn build_for_await( + iterator: Expression<'a>, + step_key: &BoundIdentifier<'a>, + body: ArenaVec<'a, Statement<'a>>, + for_of_scope_id: ScopeId, + parent_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> ArenaVec<'a, Statement<'a>> { + let var_scope_id = ctx.current_scope_id(); + + let iterator_had_error_key = + ctx.generate_uid("didIteratorError", var_scope_id, SymbolFlags::FunctionScopedVariable); + let iterator_abrupt_completion = ctx.generate_uid( + "iteratorAbruptCompletion", + var_scope_id, + SymbolFlags::FunctionScopedVariable, + ); + let iterator_error_key = + ctx.generate_uid("iteratorError", var_scope_id, SymbolFlags::FunctionScopedVariable); + + let mut items = ctx.ast.vec_with_capacity(4); + items.push(Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + iterator_abrupt_completion.create_binding_pattern(ctx), + Some(ctx.ast.expression_boolean_literal(SPAN, false)), + false, + )), + false, + ))); + items.push(Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + iterator_had_error_key.create_binding_pattern(ctx), + Some(ctx.ast.expression_boolean_literal(SPAN, false)), + false, + )), + false, + ))); + items.push(Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + iterator_error_key.create_binding_pattern(ctx), + None, + false, + )), + false, + ))); + + let iterator_key = + ctx.generate_uid("iterator", var_scope_id, SymbolFlags::FunctionScopedVariable); + let block = { + let block_scope_id = ctx.create_child_scope(parent_scope_id, ScopeFlags::empty()); + let for_statement_scope_id = + ctx.create_child_scope(block_scope_id, ScopeFlags::empty()); + + let for_statement = ctx.ast.statement_for_with_scope_id( + SPAN, + Some(ctx.ast.for_statement_init_variable_declaration( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec_from_array([ + ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + iterator_key.create_binding_pattern(ctx), + Some(iterator), + false, + ), + ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + step_key.create_binding_pattern(ctx), + None, + false, + ), + ]), + false, + )), + Some(ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + iterator_abrupt_completion.create_write_target(ctx), + ctx.ast.expression_unary( + SPAN, + UnaryOperator::LogicalNot, + Expression::from(ctx.ast.member_expression_static( + SPAN, + ctx.ast.expression_parenthesized( + SPAN, + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + step_key.create_write_target(ctx), + ctx.ast.expression_await( + SPAN, + ctx.ast.expression_call( + SPAN, + Expression::from(ctx.ast.member_expression_static( + SPAN, + iterator_key.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, "next"), + false, + )), + NONE, + ctx.ast.vec(), + false, + ), + ), + ), + ), + ctx.ast.identifier_name(SPAN, "done"), + false, + )), + ), + )), + Some(ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + iterator_abrupt_completion.create_write_target(ctx), + ctx.ast.expression_boolean_literal(SPAN, false), + )), + { + // Handle the for-of statement move to the body of new for-statement + let for_statement_body_scope_id = for_of_scope_id; + { + ctx.scoping_mut().change_scope_parent_id( + for_statement_body_scope_id, + Some(for_statement_scope_id), + ); + } + + ctx.ast.statement_block_with_scope_id(SPAN, body, for_of_scope_id) + }, + for_statement_scope_id, + ); + + ctx.ast.block_statement_with_scope_id(SPAN, ctx.ast.vec1(for_statement), block_scope_id) + }; + + let catch_clause = { + let catch_scope_id = ctx.create_child_scope(parent_scope_id, ScopeFlags::CatchClause); + let block_scope_id = ctx.create_child_scope(catch_scope_id, ScopeFlags::empty()); + let err_ident = ctx.generate_binding( + Atom::from("err"), + block_scope_id, + SymbolFlags::CatchVariable | SymbolFlags::FunctionScopedVariable, + ); + Some(ctx.ast.catch_clause_with_scope_id( + SPAN, + Some(ctx.ast.catch_parameter(SPAN, err_ident.create_binding_pattern(ctx))), + { + ctx.ast.block_statement_with_scope_id( + SPAN, + ctx.ast.vec_from_array([ + ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + iterator_had_error_key.create_write_target(ctx), + ctx.ast.expression_boolean_literal(SPAN, true), + ), + ), + ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + iterator_error_key.create_write_target(ctx), + err_ident.create_read_expression(ctx), + ), + ), + ]), + block_scope_id, + ) + }, + catch_scope_id, + )) + }; + + let finally = { + let finally_scope_id = ctx.create_child_scope(parent_scope_id, ScopeFlags::empty()); + let try_statement = { + let try_block_scope_id = + ctx.create_child_scope(finally_scope_id, ScopeFlags::empty()); + let if_statement = { + let if_block_scope_id = + ctx.create_child_scope(try_block_scope_id, ScopeFlags::empty()); + ctx.ast.statement_if( + SPAN, + ctx.ast.expression_logical( + SPAN, + iterator_abrupt_completion.create_read_expression(ctx), + LogicalOperator::And, + ctx.ast.expression_binary( + SPAN, + Expression::from(ctx.ast.member_expression_static( + SPAN, + iterator_key.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, "return"), + false, + )), + BinaryOperator::Inequality, + ctx.ast.expression_null_literal(SPAN), + ), + ), + ctx.ast.statement_block_with_scope_id( + SPAN, + ctx.ast.vec1(ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_await( + SPAN, + ctx.ast.expression_call( + SPAN, + Expression::from(ctx.ast.member_expression_static( + SPAN, + iterator_key.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, "return"), + false, + )), + NONE, + ctx.ast.vec(), + false, + ), + ), + )), + if_block_scope_id, + ), + None, + ) + }; + let block = ctx.ast.block_statement_with_scope_id( + SPAN, + ctx.ast.vec1(if_statement), + try_block_scope_id, + ); + let finally = { + let finally_scope_id = + ctx.create_child_scope(finally_scope_id, ScopeFlags::empty()); + let if_statement = { + let if_block_scope_id = + ctx.create_child_scope(finally_scope_id, ScopeFlags::empty()); + ctx.ast.statement_if( + SPAN, + iterator_had_error_key.create_read_expression(ctx), + ctx.ast.statement_block_with_scope_id( + SPAN, + ctx.ast.vec1(ctx.ast.statement_throw( + SPAN, + iterator_error_key.create_read_expression(ctx), + )), + if_block_scope_id, + ), + None, + ) + }; + ctx.ast.block_statement_with_scope_id( + SPAN, + ctx.ast.vec1(if_statement), + finally_scope_id, + ) + }; + ctx.ast.statement_try(SPAN, block, NONE, Some(finally)) + }; + + let block_statement = ctx.ast.block_statement_with_scope_id( + SPAN, + ctx.ast.vec1(try_statement), + finally_scope_id, + ); + Some(block_statement) + }; + + let try_statement = ctx.ast.statement_try(SPAN, block, catch_clause, finally); + + items.push(try_statement); + items + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2018/async_generator_functions/mod.rs b/crates/swc_ecma_transformer/oxc/es2018/async_generator_functions/mod.rs new file mode 100644 index 000000000000..689fb7647efb --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2018/async_generator_functions/mod.rs @@ -0,0 +1,235 @@ +//! ES2018: Async Generator Functions +//! +//! This plugin mainly does the following transformations: +//! +//! 1. transforms async generator functions (async function *name() {}) to generator functions +//! and wraps them with `awaitAsyncGenerator` helper function. +//! 2. transforms `await expr` expression to `yield awaitAsyncGenerator(expr)`. +//! 3. transforms `yield * argument` expression to `yield asyncGeneratorDelegate(asyncIterator(argument))`. +//! 4. transforms `for await` statement to `for` statement, and inserts many code to handle async iteration. +//! +//! ## Example +//! +//! Input: +//! ```js +//! async function f() { +//! for await (let x of y) { +//! g(x); +//! } +//!} +//! ``` +//! +//! Output: +//! ```js +//! function f() { +//! return _f.apply(this, arguments); +//! } +//! function _f() { +//! _f = babelHelpers.asyncToGenerator(function* () { +//! var _iteratorAbruptCompletion = false; +//! var _didIteratorError = false; +//! var _iteratorError; +//! try { +//! for (var _iterator = babelHelpers.asyncIterator(y), _step; _iteratorAbruptCompletion = !(_step = yield _iterator.next()).done; _iteratorAbruptCompletion = false) { +//! let x = _step.value; +//! { +//! g(x); +//! } +//! } +//! } catch (err) { +//! _didIteratorError = true; +//! _iteratorError = err; +//! } finally { +//! try { +//! if (_iteratorAbruptCompletion && _iterator.return != null) { +//! yield _iterator.return(); +//! } +//! } finally { +//! if (_didIteratorError) { +//! throw _iteratorError; +//! } +//! } +//! } +//! }); +//! return _f.apply(this, arguments); +//! } +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-async-generator-functions](https://babel.dev/docs/babel-plugin-transform-async-generator-functions). +//! +//! Reference: +//! * Babel docs: +//! * Babel implementation: +//! * Async Iteration TC39 proposal: + +mod for_await; + +use oxc_allocator::TakeIn; +use oxc_ast::ast::*; +use oxc_span::SPAN; +use oxc_traverse::{Ancestor, Traverse}; + +use crate::{ + common::helper_loader::Helper, + context::{TransformCtx, TraverseCtx}, + es2017::AsyncGeneratorExecutor, + state::TransformState, +}; + +pub struct AsyncGeneratorFunctions<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + executor: AsyncGeneratorExecutor<'a, 'ctx>, +} + +impl<'a, 'ctx> AsyncGeneratorFunctions<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx, executor: AsyncGeneratorExecutor::new(Helper::WrapAsyncGenerator, ctx) } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for AsyncGeneratorFunctions<'a, '_> { + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let new_expr = match expr { + Expression::AwaitExpression(await_expr) => { + self.transform_await_expression(await_expr, ctx) + } + Expression::YieldExpression(yield_expr) => { + self.transform_yield_expression(yield_expr, ctx) + } + Expression::FunctionExpression(func) => { + if func.r#async && func.generator { + Some(self.executor.transform_function_expression(func, ctx)) + } else { + None + } + } + _ => None, + }; + + if let Some(new_expr) = new_expr { + *expr = new_expr; + } + } + + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + self.transform_statement(stmt, ctx); + } + + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let function = match stmt { + Statement::FunctionDeclaration(func) => Some(func), + Statement::ExportDefaultDeclaration(decl) => { + if let ExportDefaultDeclarationKind::FunctionDeclaration(func) = + &mut decl.declaration + { + Some(func) + } else { + None + } + } + Statement::ExportNamedDeclaration(decl) => { + if let Some(Declaration::FunctionDeclaration(func)) = &mut decl.declaration { + Some(func) + } else { + None + } + } + _ => None, + }; + + if let Some(function) = function + && function.r#async + && function.generator + && !function.is_typescript_syntax() + { + let new_statement = self.executor.transform_function_declaration(function, ctx); + self.ctx.statement_injector.insert_after(stmt, new_statement); + } + } + + fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if func.r#async + && func.generator + && !func.is_typescript_syntax() + && AsyncGeneratorExecutor::is_class_method_like_ancestor(ctx.parent()) + { + self.executor.transform_function_for_method_definition(func, ctx); + } + } +} + +impl<'a> AsyncGeneratorFunctions<'a, '_> { + /// Transform `yield * argument` expression to `yield asyncGeneratorDelegate(asyncIterator(argument))`. + fn transform_yield_expression( + &self, + expr: &mut YieldExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if !expr.delegate || !Self::yield_is_inside_async_generator_function(ctx) { + return None; + } + + expr.argument.as_mut().map(|argument| { + let argument = Argument::from(argument.take_in(ctx.ast)); + let arguments = ctx.ast.vec1(argument); + let mut argument = + self.ctx.helper_call_expr(Helper::AsyncIterator, SPAN, arguments, ctx); + let arguments = ctx.ast.vec1(Argument::from(argument)); + argument = + self.ctx.helper_call_expr(Helper::AsyncGeneratorDelegate, SPAN, arguments, ctx); + ctx.ast.expression_yield(SPAN, expr.delegate, Some(argument)) + }) + } + + /// Check whether `yield` is inside an async generator function. + fn yield_is_inside_async_generator_function(ctx: &TraverseCtx<'a>) -> bool { + for ancestor in ctx.ancestors() { + // Note: `yield` cannot appear within function params. + // Also cannot appear in arrow functions because no such thing as a generator arrow function. + if let Ancestor::FunctionBody(func) = ancestor { + return *func.r#async(); + } + } + // `yield` can only appear in a function + unreachable!(); + } + + /// Transforms `await expr` expression to `yield awaitAsyncGenerator(expr)`. + /// Ignores top-level await expression. + fn transform_await_expression( + &self, + expr: &mut AwaitExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if !Self::async_is_inside_async_generator_function(ctx) { + return None; + } + + let mut argument = expr.argument.take_in(ctx.ast); + let arguments = ctx.ast.vec1(Argument::from(argument)); + argument = self.ctx.helper_call_expr(Helper::AwaitAsyncGenerator, SPAN, arguments, ctx); + + Some(ctx.ast.expression_yield(SPAN, false, Some(argument))) + } + + /// Check whether `await` is inside an async generator function. + fn async_is_inside_async_generator_function(ctx: &TraverseCtx<'a>) -> bool { + for ancestor in ctx.ancestors() { + match ancestor { + // Note: `await` cannot appear within function params + Ancestor::FunctionBody(func) => { + return *func.generator(); + } + Ancestor::ArrowFunctionExpressionBody(_) => { + // Arrow functions can't be generator functions + return false; + } + _ => {} + } + } + // Top level await + false + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2018/mod.rs b/crates/swc_ecma_transformer/oxc/es2018/mod.rs new file mode 100644 index 000000000000..098b9391d4b5 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2018/mod.rs @@ -0,0 +1,121 @@ +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +mod async_generator_functions; +mod object_rest_spread; +mod options; + +pub use async_generator_functions::AsyncGeneratorFunctions; +pub use object_rest_spread::{ObjectRestSpread, ObjectRestSpreadOptions}; +pub use options::ES2018Options; + +pub struct ES2018<'a, 'ctx> { + options: ES2018Options, + + // Plugins + object_rest_spread: ObjectRestSpread<'a, 'ctx>, + async_generator_functions: AsyncGeneratorFunctions<'a, 'ctx>, +} + +impl<'a, 'ctx> ES2018<'a, 'ctx> { + pub fn new(options: ES2018Options, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + object_rest_spread: ObjectRestSpread::new( + options.object_rest_spread.unwrap_or_default(), + ctx, + ), + async_generator_functions: AsyncGeneratorFunctions::new(ctx), + options, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2018<'a, '_> { + fn exit_program(&mut self, program: &mut oxc_ast::ast::Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.object_rest_spread.is_some() { + self.object_rest_spread.exit_program(program, ctx); + } + } + + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.object_rest_spread.is_some() { + self.object_rest_spread.enter_expression(expr, ctx); + } + } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_generator_functions { + self.async_generator_functions.exit_expression(expr, ctx); + } + } + + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_generator_functions { + self.async_generator_functions.enter_statement(stmt, ctx); + } + } + + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_generator_functions { + self.async_generator_functions.exit_statement(stmt, ctx); + } + } + + fn enter_for_in_statement(&mut self, stmt: &mut ForInStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.object_rest_spread.is_some() { + self.object_rest_spread.enter_for_in_statement(stmt, ctx); + } + } + + fn enter_for_of_statement(&mut self, stmt: &mut ForOfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_generator_functions { + self.async_generator_functions.enter_for_of_statement(stmt, ctx); + } + if self.options.object_rest_spread.is_some() { + self.object_rest_spread.enter_for_of_statement(stmt, ctx); + } + } + + fn enter_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.object_rest_spread.is_some() { + self.object_rest_spread.enter_arrow_function_expression(arrow, ctx); + } + } + + fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.object_rest_spread.is_some() { + self.object_rest_spread.enter_function(func, ctx); + } + } + + fn exit_function(&mut self, node: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_generator_functions { + self.async_generator_functions.exit_function(node, ctx); + } + } + + fn enter_variable_declaration( + &mut self, + decl: &mut VariableDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.object_rest_spread.is_some() { + self.object_rest_spread.enter_variable_declaration(decl, ctx); + } + } + + fn enter_catch_clause(&mut self, clause: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.object_rest_spread.is_some() { + self.object_rest_spread.enter_catch_clause(clause, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2018/object_rest_spread.rs b/crates/swc_ecma_transformer/oxc/es2018/object_rest_spread.rs new file mode 100644 index 000000000000..0aab31f8ef58 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2018/object_rest_spread.rs @@ -0,0 +1,1192 @@ +//! ES2018 object spread transformation. +//! +//! This plugin transforms rest properties for object destructuring assignment and spread properties for object literals. +//! +//! > This plugin is included in `preset-env`, in ES2018 +//! +//! ## Example +//! +//! Input: +//! ```js +//! var x = { a: 1, b: 2 }; +//! var y = { ...x, c: 3 }; +//! ``` +//! +//! Output: +//! ```js +//! var x = { a: 1, b: 2 }; +//! var y = _objectSpread({}, x, { c: 3 }); +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-object-rest-spread](https://babeljs.io/docs/babel-plugin-transform-object-rest-spread). +//! +//! ## References: +//! +//! * Babel plugin implementation: +//! * Object rest/spread TC39 proposal: + +use std::mem; + +use serde::Deserialize; + +use oxc_allocator::{Box as ArenaBox, GetAddress, TakeIn, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_ecmascript::{BoundNames, ToJsString, WithoutGlobalReferenceInformation}; +use oxc_semantic::{ScopeFlags, ScopeId, SymbolFlags}; +use oxc_span::{GetSpan, SPAN}; +use oxc_traverse::{Ancestor, MaybeBoundIdentifier, Traverse}; + +use crate::{ + common::helper_loader::Helper, + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct ObjectRestSpreadOptions { + pub loose: bool, + + pub use_built_ins: bool, +} + +pub struct ObjectRestSpread<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + + options: ObjectRestSpreadOptions, + + excluded_variable_declarators: Vec>, +} + +impl<'a, 'ctx> ObjectRestSpread<'a, 'ctx> { + pub fn new(options: ObjectRestSpreadOptions, ctx: &'ctx TransformCtx<'a>) -> Self { + if options.loose { + ctx.error(OxcDiagnostic::error( + "Option `loose` is not implemented for object-rest-spread.", + )); + } + if options.use_built_ins { + ctx.error(OxcDiagnostic::error( + "Option `useBuiltIns` is not implemented for object-rest-spread.", + )); + } + if ctx.assumptions.object_rest_no_symbols { + ctx.error(OxcDiagnostic::error( + "Compiler assumption `objectRestNoSymbols` is not implemented for object-rest-spread.", + )); + } + if ctx.assumptions.ignore_function_length { + ctx.error(OxcDiagnostic::error( + "Compiler assumption `ignoreFunctionLength` is not implemented for object-rest-spread.", + )); + } + Self { ctx, options, excluded_variable_declarators: vec![] } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ObjectRestSpread<'a, '_> { + // For excluded keys when destructuring inside a function. + // `function foo() { ({a, ...b} = c) }` -> `const _excluded = ["a"]; function foo() { ... }` + fn exit_program(&mut self, _node: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if !self.excluded_variable_declarators.is_empty() { + let declarators = ctx.ast.vec_from_iter(self.excluded_variable_declarators.drain(..)); + let kind = VariableDeclarationKind::Const; + let declaration = ctx.ast.alloc_variable_declaration(SPAN, kind, declarators, false); + let statement = Statement::VariableDeclaration(declaration); + self.ctx.top_level_statements.insert_statement(statement); + } + } + + // `({ x, ..y })`. + // `({ x, ..y } = foo)`. + // `([{ x, ..y }] = foo)`. + #[inline] + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + match expr { + Expression::ObjectExpression(_) => { + Self::transform_object_expression(self.options, expr, self.ctx, ctx); + } + Expression::AssignmentExpression(_) => { + self.transform_assignment_expression(expr, ctx); + } + _ => {} + } + } + + // `(...x) => {}`. + #[inline] + fn enter_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + Self::transform_arrow(arrow, ctx); + } + + // `function foo({...x}) {}`. + #[inline] + fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + Self::transform_function(func, ctx); + } + + // `let { x, ..y } = foo`. + // `let [{ x, ..y }] = foo` + // Includes `for (var {...x} = 1;;);` + #[inline] + fn enter_variable_declaration( + &mut self, + decl: &mut VariableDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.transform_variable_declaration(decl, ctx); + } + + // Transform `try {} catch (...x) {}`. + #[inline] + fn enter_catch_clause(&mut self, clause: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) { + if clause.param.is_some() { + Self::transform_catch_clause(clause, ctx); + } + } + + // `for ({...x} in []);` `for ({...x} of []);` + // `for ([{...x}] in []);` `for ([{...x}] of []);` + #[inline] + fn enter_for_in_statement(&mut self, stmt: &mut ForInStatement<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = stmt.scope_id(); + match &mut stmt.left { + ForStatementLeft::VariableDeclaration(decl) => { + let body = &mut stmt.body; + Self::transform_variable_declaration_for_x_statement(decl, body, scope_id, ctx); + } + _ => { + Self::transform_for_statement_left(scope_id, &mut stmt.left, &mut stmt.body, ctx); + } + } + } + + // `for ({...x} in []);` `for ({...x} of []);` + // `for ([{...x}] in []);` `for ([{...x}] of []);` + #[inline] + fn enter_for_of_statement(&mut self, stmt: &mut ForOfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = stmt.scope_id(); + match &mut stmt.left { + ForStatementLeft::VariableDeclaration(decl) => { + let body = &mut stmt.body; + Self::transform_variable_declaration_for_x_statement(decl, body, scope_id, ctx); + } + _ => { + Self::transform_for_statement_left(scope_id, &mut stmt.left, &mut stmt.body, ctx); + } + } + } +} + +impl<'a> ObjectRestSpread<'a, '_> { + // Transform `({ x, ..y } = foo)`. + // Transform `([{ x, ..y }] = foo)`. + fn transform_assignment_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + // Allow `{...x} = {}` and `[{...x}] = []`. + if !Self::has_nested_target_rest(&assign_expr.left) { + return; + } + + // If not an top `({ ...y })`, walk the pattern and create references for all the objects with a rest. + if !matches!(&assign_expr.left, AssignmentTarget::ObjectAssignmentTarget(t) if t.rest.is_some()) + { + self.walk_and_replace_nested_object_target(expr, ctx); + return; + } + + let kind = VariableDeclarationKind::Var; + let symbol_flags = kind_to_symbol_flags(kind); + let scope_id = ctx.current_scope_id(); + let mut reference_builder = + ReferenceBuilder::new(&mut assign_expr.right, symbol_flags, scope_id, true, ctx); + let state = State::new(kind, symbol_flags, scope_id); + + let mut new_decls = vec![]; + + if let Some(id) = reference_builder.binding.take() { + new_decls.push(ctx.ast.variable_declarator(SPAN, state.kind, id, None, false)); + } + + let data = Self::walk_assignment_target(&mut assign_expr.left, &mut new_decls, state, ctx); + + // Insert `var _foo` before this statement. + if !new_decls.is_empty() { + for node in ctx.ancestors() { + if let Ancestor::ExpressionStatementExpression(decl) = node { + let kind = VariableDeclarationKind::Var; + let declaration = ctx.ast.alloc_variable_declaration( + SPAN, + kind, + ctx.ast.vec_from_iter(new_decls), + false, + ); + let statement = Statement::VariableDeclaration(declaration); + self.ctx.statement_injector.insert_before(&decl.address(), statement); + break; + } + } + } + + // Make an sequence expression. + let mut expressions = ctx.ast.vec(); + let op = AssignmentOperator::Assign; + + // Insert `_foo = rhs` + if let Some(expr) = reference_builder.expr.take() { + expressions.push(ctx.ast.expression_assignment( + SPAN, + op, + reference_builder.maybe_bound_identifier.create_write_target(ctx), + expr, + )); + } + + // Insert `{} = _foo` + expressions.push(ctx.ast.expression_assignment( + SPAN, + op, + assign_expr.left.take_in(ctx.ast), + reference_builder.create_read_expression(ctx), + )); + + // Insert all `rest = _extends({}, (_objectDestructuringEmpty(_foo), _foo))` + for datum in data { + let (lhs, rhs) = datum.get_lhs_rhs( + &mut reference_builder, + &mut self.excluded_variable_declarators, + self.ctx, + ctx, + ); + if let BindingPatternOrAssignmentTarget::AssignmentTarget(lhs) = lhs { + expressions.push(ctx.ast.expression_assignment(SPAN, op, lhs, rhs)); + } + } + + // Insert final read `_foo`. + // TODO: remove this if the assignment is not a read reference. + // e.g. remove for `({ a2, ...b2 } = c2)`, keep `(x, ({ a2, ...b2 } = c2)`. + expressions.push(reference_builder.create_read_expression(ctx)); + + *expr = ctx.ast.expression_sequence(assign_expr.span, expressions); + } + + fn walk_assignment_target( + target: &mut AssignmentTarget<'a>, + new_decls: &mut Vec>, + state: State, + ctx: &mut TraverseCtx<'a>, + ) -> Vec> { + match target { + AssignmentTarget::ObjectAssignmentTarget(t) => { + let mut data = vec![]; + for prop in &mut t.properties { + if let AssignmentTargetProperty::AssignmentTargetPropertyProperty(p) = prop { + data.extend(match &mut p.binding { + AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(t) => { + Self::walk_assignment_target(&mut t.binding, new_decls, state, ctx) + } + _ => Self::walk_assignment_target( + p.binding.to_assignment_target_mut(), + new_decls, + state, + ctx, + ), + }); + } + } + if let Some(datum) = + Self::transform_object_assignment_target(t, new_decls, state, ctx) + { + data.push(datum); + } + data + } + _ => vec![], + } + } + + fn transform_object_assignment_target( + object_assignment_target: &mut ObjectAssignmentTarget<'a>, + new_decls: &mut Vec>, + state: State, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let rest = object_assignment_target.rest.take()?; + let rest_target = rest.unbox().target; + let mut all_primitives = true; + let keys = + ctx.ast.vec_from_iter(object_assignment_target.properties.iter_mut().filter_map(|e| { + match e { + AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(ident) => { + let name = ident.binding.name; + let expr = ctx.ast.expression_string_literal(SPAN, name, None); + Some(ArrayExpressionElement::from(expr)) + } + AssignmentTargetProperty::AssignmentTargetPropertyProperty(p) => { + Self::transform_property_key( + &mut p.name, + new_decls, + &mut all_primitives, + state, + ctx, + ) + } + } + })); + Some(SpreadPair { + lhs: BindingPatternOrAssignmentTarget::AssignmentTarget(rest_target), + keys, + has_no_properties: object_assignment_target.is_empty(), + all_primitives, + }) + } + + fn has_nested_target_rest(target: &AssignmentTarget<'a>) -> bool { + match target { + AssignmentTarget::ObjectAssignmentTarget(t) => { + t.rest.is_some() + || t.properties.iter().any(|p| match p { + AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(_) => false, + AssignmentTargetProperty::AssignmentTargetPropertyProperty(p) => { + match &p.binding { + AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(t) => { + Self::has_nested_target_rest(&t.binding) + } + _ => Self::has_nested_target_rest(p.binding.to_assignment_target()), + } + } + }) + } + AssignmentTarget::ArrayAssignmentTarget(t) => { + t.elements.iter().flatten().any(|e| match e { + AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(t) => { + Self::has_nested_target_rest(&t.binding) + } + _ => Self::has_nested_target_rest(e.to_assignment_target()), + }) || t.rest.as_ref().is_some_and(|r| Self::has_nested_target_rest(&r.target)) + } + _ => false, + } + } + + fn walk_and_replace_nested_object_target( + &self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr else { + return; + }; + let mut decls = vec![]; + let mut exprs = vec![]; + Self::recursive_walk_assignment_target(&mut assign_expr.left, &mut decls, &mut exprs, ctx); + for node in ctx.ancestors() { + if let Ancestor::ExpressionStatementExpression(decl) = node { + let kind = VariableDeclarationKind::Var; + let declaration = ctx.ast.alloc_variable_declaration( + SPAN, + kind, + ctx.ast.vec_from_iter(decls), + false, + ); + let statement = Statement::VariableDeclaration(declaration); + self.ctx.statement_injector.insert_before(&decl.address(), statement); + break; + } + } + let mut expressions = ctx.ast.vec1(expr.take_in(ctx.ast)); + expressions.extend(exprs); + *expr = ctx.ast.expression_sequence(SPAN, expressions); + } + + fn recursive_walk_assignment_target( + pat: &mut AssignmentTarget<'a>, + decls: &mut Vec>, + exprs: &mut Vec>, + ctx: &mut TraverseCtx<'a>, + ) { + match pat { + AssignmentTarget::ArrayAssignmentTarget(t) => { + for e in t.elements.iter_mut().flatten() { + Self::recursive_walk_assignment_target_maybe_default(e, decls, exprs, ctx); + } + } + AssignmentTarget::ObjectAssignmentTarget(t) => { + for p in &mut t.properties { + if let AssignmentTargetProperty::AssignmentTargetPropertyProperty(e) = p { + Self::recursive_walk_assignment_target_maybe_default( + &mut e.binding, + decls, + exprs, + ctx, + ); + } + } + if t.rest.is_none() { + return; + } + let scope_id = ctx.scoping.current_scope_id(); + let flags = SymbolFlags::FunctionScopedVariable; + let bound_identifier = ctx.generate_uid("ref", scope_id, flags); + let id = bound_identifier.create_binding_pattern(ctx); + let kind = VariableDeclarationKind::Var; + decls.push(ctx.ast.variable_declarator(SPAN, kind, id, None, false)); + exprs.push(ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + pat.take_in(ctx.ast), + bound_identifier.create_read_expression(ctx), + )); + *pat = bound_identifier.create_spanned_write_target(SPAN, ctx); + } + _ => {} + } + } + + fn recursive_walk_assignment_target_maybe_default( + target: &mut AssignmentTargetMaybeDefault<'a>, + decls: &mut Vec>, + exprs: &mut Vec>, + ctx: &mut TraverseCtx<'a>, + ) { + match target { + AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(d) => { + Self::recursive_walk_assignment_target(&mut d.binding, decls, exprs, ctx); + } + _ => Self::recursive_walk_assignment_target( + target.to_assignment_target_mut(), + decls, + exprs, + ctx, + ), + } + } +} + +impl<'a, 'ctx> ObjectRestSpread<'a, 'ctx> { + // Transform `({ x, ..y })`. + // `pub` for jsx spread. + pub fn transform_object_expression( + _options: ObjectRestSpreadOptions, + expr: &mut Expression<'a>, + transform_ctx: &'ctx TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::ObjectExpression(obj_expr) = expr else { unreachable!() }; + + if obj_expr.properties.iter().all(|prop| !prop.is_spread()) { + return; + } + + let mut call_expr: Option>> = None; + let mut props = ctx.ast.vec_with_capacity(obj_expr.properties.len()); + + for prop in obj_expr.properties.drain(..) { + if let ObjectPropertyKind::SpreadProperty(mut spread_prop) = prop { + Self::make_object_spread(&mut call_expr, &mut props, transform_ctx, ctx); + let arg = spread_prop.argument.take_in(ctx.ast); + call_expr.as_mut().unwrap().arguments.push(Argument::from(arg)); + } else { + props.push(prop); + } + } + + if !props.is_empty() { + Self::make_object_spread(&mut call_expr, &mut props, transform_ctx, ctx); + } + + *expr = Expression::CallExpression(call_expr.unwrap()); + } + + fn make_object_spread( + expr: &mut Option>>, + props: &mut ArenaVec<'a, ObjectPropertyKind<'a>>, + transform_ctx: &'ctx TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let had_props = !props.is_empty(); + let obj = ctx.ast.expression_object( + SPAN, + // Reserve maximize might be used space for new vec + mem::replace(props, ctx.ast.vec_with_capacity(props.capacity() - props.len())), + ); + let arguments = if let Some(call_expr) = expr.take() { + let arg = Expression::CallExpression(call_expr); + let arg = Argument::from(arg); + if had_props { + let empty_object = ctx.ast.expression_object(SPAN, ctx.ast.vec()); + ctx.ast.vec_from_array([arg, Argument::from(empty_object), Argument::from(obj)]) + } else { + ctx.ast.vec1(arg) + } + } else { + ctx.ast.vec1(Argument::from(obj)) + }; + let new_expr = transform_ctx.helper_call(Helper::ObjectSpread2, SPAN, arguments, ctx); + expr.replace(ctx.ast.alloc(new_expr)); + } +} + +impl<'a> ObjectRestSpread<'a, '_> { + // Transform `function foo({...x}) {}`. + fn transform_function(func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = func.scope_id(); + let Some(body) = func.body.as_mut() else { return }; + for param in &mut func.params.items { + if Self::has_nested_object_rest(¶m.pattern) { + Self::replace_rest_element( + VariableDeclarationKind::Var, + &mut param.pattern, + &mut body.statements, + scope_id, + ctx, + ); + } + } + } + + // Transform `(...x) => {}`. + fn transform_arrow(arrow: &mut ArrowFunctionExpression<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = arrow.scope_id(); + for param in &mut arrow.params.items { + if Self::has_nested_object_rest(¶m.pattern) { + // `({ ...args }) => { args }` + if arrow.expression { + arrow.expression = false; + + debug_assert!(arrow.body.statements.len() == 1); + + let Statement::ExpressionStatement(stmt) = arrow.body.statements.pop().unwrap() + else { + unreachable!( + "`arrow.expression` is true, which means it has only one ExpressionStatement." + ); + }; + let return_stmt = + ctx.ast.statement_return(stmt.span, Some(stmt.unbox().expression)); + arrow.body.statements.push(return_stmt); + } + Self::replace_rest_element( + VariableDeclarationKind::Var, + &mut param.pattern, + &mut arrow.body.statements, + scope_id, + ctx, + ); + } + } + } + + // Transform `try {} catch ({...x}) {}`. + fn transform_catch_clause(clause: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) { + let Some(param) = &mut clause.param else { unreachable!() }; + if Self::has_nested_object_rest(¶m.pattern) { + let scope_id = clause.body.scope_id(); + // Remove `SymbolFlags::CatchVariable`. + param.pattern.bound_names(&mut |ident| { + ctx.scoping_mut() + .symbol_flags_mut(ident.symbol_id()) + .remove(SymbolFlags::CatchVariable); + }); + Self::replace_rest_element( + VariableDeclarationKind::Var, + &mut param.pattern, + &mut clause.body.body, + scope_id, + ctx, + ); + } + } + + // Transform `for (var { ...x };;)`. + fn transform_variable_declaration_for_x_statement( + decl: &mut VariableDeclaration<'a>, + body: &mut Statement<'a>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) { + for declarator in &mut decl.declarations { + if Self::has_nested_object_rest(&declarator.id) { + let new_scope_id = Self::try_replace_statement_with_block(body, scope_id, ctx); + let Statement::BlockStatement(block) = body else { + unreachable!(); + }; + let mut bound_names = vec![]; + declarator.id.bound_names(&mut |ident| bound_names.push(ident.clone())); + Self::replace_rest_element( + declarator.kind, + &mut declarator.id, + &mut block.body, + if decl.kind.is_var() { ctx.current_hoist_scope_id() } else { scope_id }, + ctx, + ); + // Move the bindings from the for init scope to scope of the loop body. + for ident in bound_names { + ctx.scoping_mut().set_symbol_scope_id(ident.symbol_id(), new_scope_id); + ctx.scoping_mut().move_binding(scope_id, new_scope_id, ident.name.into()); + } + } + } + } + + // If the assignment target contains an object rest, + // create a reference and move the assignment target to the block body. + // `for ({...x} in []);` `for ({...x} of []);` + // `for ([{...x}] in []);` `for ([{...x}] of []);` + fn transform_for_statement_left( + scope_id: ScopeId, + left: &mut ForStatementLeft<'a>, + body: &mut Statement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !Self::has_nested_target_rest(left.to_assignment_target()) { + return; + } + let target = left.to_assignment_target_mut(); + let assign_left = target.take_in(ctx.ast); + let flags = SymbolFlags::FunctionScopedVariable; + let bound_identifier = ctx.generate_uid("ref", scope_id, flags); + let id = bound_identifier.create_binding_pattern(ctx); + let kind = VariableDeclarationKind::Var; + let declarations = ctx.ast.vec1(ctx.ast.variable_declarator(SPAN, kind, id, None, false)); + let decl = ctx.ast.alloc_variable_declaration(SPAN, kind, declarations, false); + *left = ForStatementLeft::VariableDeclaration(decl); + Self::try_replace_statement_with_block(body, scope_id, ctx); + let Statement::BlockStatement(block) = body else { + unreachable!(); + }; + let operator = AssignmentOperator::Assign; + let right = bound_identifier.create_read_expression(ctx); + let expr = ctx.ast.expression_assignment(SPAN, operator, assign_left, right); + let stmt = ctx.ast.statement_expression(SPAN, expr); + block.body.insert(0, stmt); + } + + fn try_replace_statement_with_block( + stmt: &mut Statement<'a>, + parent_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> ScopeId { + if let Statement::BlockStatement(block) = stmt { + return block.scope_id(); + } + let scope_id = ctx.create_child_scope(parent_scope_id, ScopeFlags::empty()); + let (span, stmts) = if let Statement::EmptyStatement(empty_stmt) = stmt { + (empty_stmt.span, ctx.ast.vec()) + } else { + let span = stmt.span(); + (span, ctx.ast.vec1(stmt.take_in(ctx.ast))) + }; + *stmt = ctx.ast.statement_block_with_scope_id(span, stmts, scope_id); + scope_id + } + + /// Recursively check for object rest. + fn has_nested_object_rest(pat: &BindingPattern<'a>) -> bool { + match &pat.kind { + BindingPatternKind::ObjectPattern(pat) => { + pat.rest.is_some() + || pat.properties.iter().any(|p| Self::has_nested_object_rest(&p.value)) + } + BindingPatternKind::ArrayPattern(pat) => { + pat.elements.iter().any(|e| e.as_ref().is_some_and(Self::has_nested_object_rest)) + || pat.rest.as_ref().is_some_and(|e| Self::has_nested_object_rest(&e.argument)) + } + BindingPatternKind::AssignmentPattern(pat) => Self::has_nested_object_rest(&pat.left), + BindingPatternKind::BindingIdentifier(_) => false, + } + } + + /// Move the binding to the body if it contains an object rest. + /// The object pattern will be transform by `transform_object_pattern` afterwards. + fn replace_rest_element( + kind: VariableDeclarationKind, + pattern: &mut BindingPattern<'a>, + body: &mut ArenaVec<'a, Statement<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) { + match &mut pattern.kind { + // Replace the object pattern, no need to walk the object properties. + BindingPatternKind::ObjectPattern(_) => { + Self::replace_object_pattern_and_insert_into_block_body( + kind, pattern, body, scope_id, ctx, + ); + } + BindingPatternKind::AssignmentPattern(pat) => { + Self::replace_object_pattern_and_insert_into_block_body( + kind, + &mut pat.left, + body, + scope_id, + ctx, + ); + } + // Or replace all occurrences of object pattern inside array pattern. + BindingPatternKind::ArrayPattern(pat) => { + for element in pat.elements.iter_mut().flatten() { + Self::replace_rest_element(kind, element, body, scope_id, ctx); + } + if let Some(element) = &mut pat.rest { + Self::replace_rest_element(kind, &mut element.argument, body, scope_id, ctx); + } + } + BindingPatternKind::BindingIdentifier(_) => {} + } + } + + // Add `let {...x} = _ref` to body. + fn replace_object_pattern_and_insert_into_block_body( + kind: VariableDeclarationKind, + pat: &mut BindingPattern<'a>, + body: &mut ArenaVec<'a, Statement<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) { + let decl = Self::create_temporary_reference_for_binding(kind, pat, scope_id, ctx); + body.insert(0, Statement::VariableDeclaration(ctx.ast.alloc(decl))); + } + + fn create_temporary_reference_for_binding( + kind: VariableDeclarationKind, + pat: &mut BindingPattern<'a>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> VariableDeclaration<'a> { + let mut flags = kind_to_symbol_flags(kind); + if matches!(ctx.parent(), Ancestor::TryStatementHandler(_)) { + // try {} catch (ref) {} + // ^^^ + flags |= SymbolFlags::CatchVariable; + } + let bound_identifier = ctx.generate_uid("ref", scope_id, flags); + let kind = VariableDeclarationKind::Let; + let id = mem::replace(pat, bound_identifier.create_binding_pattern(ctx)); + let init = bound_identifier.create_read_expression(ctx); + let declarations = + ctx.ast.vec1(ctx.ast.variable_declarator(SPAN, kind, id, Some(init), false)); + let decl = ctx.ast.variable_declaration(SPAN, kind, declarations, false); + decl.bound_names(&mut |ident| { + *ctx.scoping_mut().symbol_flags_mut(ident.symbol_id()) = + SymbolFlags::BlockScopedVariable; + }); + decl + } +} + +impl<'a> ObjectRestSpread<'a, '_> { + // Transform `let { x, ..y } = foo`. + // Transform `let [{ x, ..y }] = foo`. + fn transform_variable_declaration( + &mut self, + decl: &mut VariableDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let mut new_decls = vec![]; + for (i, variable_declarator) in decl.declarations.iter_mut().enumerate() { + if variable_declarator.init.is_some() + && Self::has_nested_object_rest(&variable_declarator.id) + { + let decls = self.transform_variable_declarator(variable_declarator, ctx); + new_decls.push((i, decls)); + } + } + for (i, decls) in new_decls { + decl.declarations.splice(i..=i, decls); + } + } + + // The algorithm depth searches for object rest, + // and then creates a ref for the object, subsequent visit will then transform this object rest. + // Transforms: + // * `var {...a} = foo` -> `var a = _extends({}, (_objectDestructuringEmpty(foo), foo));` + // * `var [{x: {y: {...a}}}] = z` -> `var [{x: {y: _ref}}] = z, {...a} = _ref` + fn transform_variable_declarator( + &mut self, + decl: &mut VariableDeclarator<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ArenaVec<'a, VariableDeclarator<'a>> { + // It is syntax error or inside for loop if missing initializer in destructuring pattern. + let init = decl.init.as_mut().unwrap(); + + // Use the scope of the identifier, scope is different for + // `for (var {...x} = {};;);` and `for (let {...x} = {};;);` + // TODO: improve this by getting the value only once. + let mut scope_id = ctx.current_scope_id(); + let mut symbol_flags = kind_to_symbol_flags(decl.kind); + let symbols = ctx.scoping(); + decl.id.bound_names(&mut |ident| { + let symbol_id = ident.symbol_id(); + scope_id = symbols.symbol_scope_id(symbol_id); + symbol_flags.insert(symbols.symbol_flags(symbol_id)); + }); + + let state = State::new(decl.kind, symbol_flags, scope_id); + let mut new_decls = vec![]; + + let mut reference_builder = ReferenceBuilder::new(init, symbol_flags, scope_id, false, ctx); + let remove_empty_object_pattern; + + // Add `_foo = foo()` + if let Some(id) = reference_builder.binding.take() { + let decl = ctx.ast.variable_declarator( + SPAN, + state.kind, + id, + Some(reference_builder.create_read_expression(ctx)), + false, + ); + new_decls.push(decl); + } + + let mut temp_decls = vec![]; + let mut temp_keys = vec![]; + + if let BindingPatternKind::ObjectPattern(pat) = &mut decl.id.kind { + // Example: `let { x, ...rest } = foo();`. + remove_empty_object_pattern = pat.properties.is_empty(); + // Walk the properties that may contain a nested rest spread. + let data = pat + .properties + .iter_mut() + .flat_map(|p| self.recursive_walk_binding_pattern(&mut p.value, state, ctx)) + .collect::>(); + temp_decls.extend(data); + + // Transform the object pattern with a rest pattern. + if let Some(rest) = pat.rest.take() { + let lhs = BindingPatternOrAssignmentTarget::BindingPattern(rest.unbox().argument); + let mut all_primitives = true; + // Create the access keys. + // `let { a, b, ...c } = foo` -> `["a", "b"]` + let keys = ctx.ast.vec_from_iter(pat.properties.iter_mut().filter_map( + |binding_property| { + Self::transform_property_key( + &mut binding_property.key, + &mut temp_keys, + &mut all_primitives, + state, + ctx, + ) + }, + )); + let datum = SpreadPair { + lhs, + keys, + has_no_properties: pat.properties.is_empty(), + all_primitives, + }; + // Add `rest = babelHelpers.extends({}, (babelHelpers.objectDestructuringEmpty(_foo), _foo))`. + // Or `rest = babelHelpers.objectWithoutProperties(_foo, ["x"])`. + let (lhs, rhs) = datum.get_lhs_rhs( + &mut reference_builder, + &mut self.excluded_variable_declarators, + self.ctx, + ctx, + ); + if let BindingPatternOrAssignmentTarget::BindingPattern(lhs) = lhs { + let decl = + ctx.ast.variable_declarator(lhs.span(), decl.kind, lhs, Some(rhs), false); + temp_decls.push(decl); + } + } + } else { + remove_empty_object_pattern = false; + let data = self.recursive_walk_binding_pattern(&mut decl.id, state, ctx); + temp_decls.extend(data); + } + + new_decls.extend(temp_keys); + + // Insert the original declarator by copying its data out. + if !remove_empty_object_pattern { + let mut binding_pattern_kind = + ctx.ast.binding_pattern_kind_object_pattern(decl.span, ctx.ast.vec(), NONE); + mem::swap(&mut binding_pattern_kind, &mut decl.id.kind); + let decl = ctx.ast.variable_declarator( + decl.span, + decl.kind, + ctx.ast.binding_pattern(binding_pattern_kind, NONE, false), + Some(reference_builder.create_read_expression(ctx)), + false, + ); + new_decls.push(decl); + } + + new_decls.extend(temp_decls); + ctx.ast.vec_from_iter(new_decls) + } + + // Returns all temporary references + fn recursive_walk_binding_pattern( + &mut self, + pat: &mut BindingPattern<'a>, + state: State, + ctx: &mut TraverseCtx<'a>, + ) -> Vec> { + match &mut pat.kind { + BindingPatternKind::BindingIdentifier(_) => vec![], + BindingPatternKind::ArrayPattern(array_pat) => array_pat + .elements + .iter_mut() + .flatten() + .flat_map(|p| self.recursive_walk_binding_pattern(p, state, ctx)) + .collect::>(), + BindingPatternKind::AssignmentPattern(assign_pat) => { + self.recursive_walk_binding_pattern(&mut assign_pat.left, state, ctx) + } + BindingPatternKind::ObjectPattern(p) => { + let data = p + .properties + .iter_mut() + .flat_map(|p| self.recursive_walk_binding_pattern(&mut p.value, state, ctx)) + .collect::>(); + if p.rest.is_some() { + let bound_identifier = + ctx.generate_uid("ref", state.scope_id, state.symbol_flags); + let id = mem::replace(pat, bound_identifier.create_binding_pattern(ctx)); + + let init = bound_identifier.create_read_expression(ctx); + let mut decl = + ctx.ast.variable_declarator(SPAN, state.kind, id, Some(init), false); + let mut decls = self + .transform_variable_declarator(&mut decl, ctx) + .into_iter() + .collect::>(); + decls.extend(data); + return decls; + } + data + } + } + } + + // Create a reference for computed property keys. + fn transform_property_key( + key: &mut PropertyKey<'a>, + new_decls: &mut Vec>, + all_primitives: &mut bool, + state: State, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + match key { + // `let { a, ... rest }` + PropertyKey::StaticIdentifier(ident) => { + let name = ident.name; + let expr = ctx.ast.expression_string_literal(ident.span, name, None); + Some(ArrayExpressionElement::from(expr)) + } + // `let { 'a', ... rest }` + // `let { ['a'], ... rest }` + PropertyKey::StringLiteral(lit) => { + let name = lit.value; + let expr = ctx.ast.expression_string_literal(lit.span, name, None); + Some(ArrayExpressionElement::from(expr)) + } + // `let { [`a`], ... rest }` + PropertyKey::TemplateLiteral(lit) if lit.is_no_substitution_template() => { + let quasis = ctx.ast.vec1(lit.quasis[0].clone()); + let expressions = ctx.ast.vec(); + let expr = ctx.ast.expression_template_literal(lit.span, quasis, expressions); + Some(ArrayExpressionElement::from(expr)) + } + PropertyKey::PrivateIdentifier(_) => { + /* syntax error */ + None + } + key => { + let expr = key.as_expression_mut()?; + // `let { [1], ... rest }` + if expr.is_literal() { + let span = expr.span(); + let s = expr.to_js_string(&WithoutGlobalReferenceInformation {}).unwrap(); + let s = ctx.ast.atom_from_cow(&s); + let expr = ctx.ast.expression_string_literal(span, s, None); + return Some(ArrayExpressionElement::from(expr)); + } + *all_primitives = false; + if let Expression::Identifier(ident) = expr { + let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx); + if let Some(binding) = binding.to_bound_identifier() { + let expr = binding.create_read_expression(ctx); + return Some(ArrayExpressionElement::from(expr)); + } + } + let bound_identifier = + ctx.generate_uid_based_on_node(expr, state.scope_id, state.symbol_flags); + let p = bound_identifier.create_binding_pattern(ctx); + let mut lhs = bound_identifier.create_read_expression(ctx); + mem::swap(&mut lhs, expr); + new_decls.push(ctx.ast.variable_declarator(SPAN, state.kind, p, Some(lhs), false)); + Some(ArrayExpressionElement::from(bound_identifier.create_read_expression(ctx))) + } + } + } +} + +#[derive(Debug, Clone, Copy)] +struct State { + kind: VariableDeclarationKind, + symbol_flags: SymbolFlags, + scope_id: ScopeId, +} + +impl State { + fn new(kind: VariableDeclarationKind, symbol_flags: SymbolFlags, scope_id: ScopeId) -> Self { + Self { kind, symbol_flags, scope_id } + } +} + +fn kind_to_symbol_flags(kind: VariableDeclarationKind) -> SymbolFlags { + match kind { + VariableDeclarationKind::Var => SymbolFlags::FunctionScopedVariable, + VariableDeclarationKind::Let + | VariableDeclarationKind::Using + | VariableDeclarationKind::AwaitUsing => SymbolFlags::BlockScopedVariable, + VariableDeclarationKind::Const => { + SymbolFlags::BlockScopedVariable | SymbolFlags::ConstVariable + } + } +} + +#[derive(Debug)] +enum BindingPatternOrAssignmentTarget<'a> { + BindingPattern(BindingPattern<'a>), + AssignmentTarget(AssignmentTarget<'a>), +} + +#[derive(Debug)] +struct SpreadPair<'a> { + lhs: BindingPatternOrAssignmentTarget<'a>, + keys: ArenaVec<'a, ArrayExpressionElement<'a>>, + has_no_properties: bool, + all_primitives: bool, +} + +impl<'a> SpreadPair<'a> { + fn get_lhs_rhs( + self, + reference_builder: &mut ReferenceBuilder<'a>, + excluded_variable_declarators: &mut Vec>, + transform_ctx: &TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (BindingPatternOrAssignmentTarget<'a>, Expression<'a>) { + let rhs = if self.has_no_properties { + // The `ObjectDestructuringEmpty` function throws a type error when destructuring null. + // `function _objectDestructuringEmpty(t) { if (null == t) throw new TypeError("Cannot destructure " + t); }` + let mut arguments = ctx.ast.vec(); + // Add `{}`. + arguments.push(Argument::ObjectExpression( + ctx.ast.alloc_object_expression(SPAN, ctx.ast.vec()), + )); + // Add `(_objectDestructuringEmpty(b), b);` + arguments.push(Argument::SequenceExpression(ctx.ast.alloc_sequence_expression( + SPAN, + { + let mut sequence = ctx.ast.vec(); + sequence.push(transform_ctx.helper_call_expr( + Helper::ObjectDestructuringEmpty, + SPAN, + ctx.ast.vec1(Argument::from(reference_builder.create_read_expression(ctx))), + ctx, + )); + sequence.push(reference_builder.create_read_expression(ctx)); + sequence + }, + ))); + transform_ctx.helper_call_expr(Helper::Extends, SPAN, arguments, ctx) + } else { + // / `let { a, b, ...c } = z` -> _objectWithoutProperties(_z, ["a", "b"]); + // / `_objectWithoutProperties(_z, ["a", "b"])` + let mut arguments = + ctx.ast.vec1(Argument::from(reference_builder.create_read_expression(ctx))); + let key_expression = ctx.ast.expression_array(SPAN, self.keys); + + let key_expression = if self.all_primitives + && ctx.scoping.current_scope_id() != ctx.scoping().root_scope_id() + { + // Move the key_expression to the root scope. + let bound_identifier = ctx.generate_uid_in_root_scope( + "excluded", + SymbolFlags::BlockScopedVariable | SymbolFlags::ConstVariable, + ); + let kind = VariableDeclarationKind::Const; + let declarator = ctx.ast.variable_declarator( + SPAN, + kind, + bound_identifier.create_binding_pattern(ctx), + Some(key_expression), + false, + ); + excluded_variable_declarators.push(declarator); + bound_identifier.create_read_expression(ctx) + } else if !self.all_primitives { + // map to `toPropertyKey` to handle the possible non-string values + // `[_ref].map(babelHelpers.toPropertyKey))` + let property = ctx.ast.identifier_name(SPAN, "map"); + let callee = Expression::StaticMemberExpression( + ctx.ast.alloc_static_member_expression(SPAN, key_expression, property, false), + ); + let arguments = ctx + .ast + .vec1(Argument::from(transform_ctx.helper_load(Helper::ToPropertyKey, ctx))); + ctx.ast.expression_call(SPAN, callee, NONE, arguments, false) + } else { + key_expression + }; + arguments.push(Argument::from(key_expression)); + transform_ctx.helper_call_expr(Helper::ObjectWithoutProperties, SPAN, arguments, ctx) + }; + (self.lhs, rhs) + } +} + +#[derive(Debug)] +struct ReferenceBuilder<'a> { + expr: Option>, + binding: Option>, + maybe_bound_identifier: MaybeBoundIdentifier<'a>, +} + +impl<'a> ReferenceBuilder<'a> { + fn new( + expr: &mut Expression<'a>, + symbol_flags: SymbolFlags, + scope_id: ScopeId, + force_create_binding: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Self { + let expr = expr.take_in(ctx.ast); + let binding; + let maybe_bound_identifier; + match &expr { + Expression::Identifier(ident) if !force_create_binding => { + binding = None; + maybe_bound_identifier = + MaybeBoundIdentifier::from_identifier_reference(ident, ctx); + } + expr => { + let bound_identifier = ctx.generate_uid_based_on_node(expr, scope_id, symbol_flags); + binding = Some(bound_identifier.create_binding_pattern(ctx)); + maybe_bound_identifier = bound_identifier.to_maybe_bound_identifier(); + } + } + Self { expr: Some(expr), binding, maybe_bound_identifier } + } + + fn create_read_expression(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + self.expr.take().unwrap_or_else(|| self.maybe_bound_identifier.create_read_expression(ctx)) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2018/options.rs b/crates/swc_ecma_transformer/oxc/es2018/options.rs new file mode 100644 index 000000000000..e391d8eeb660 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2018/options.rs @@ -0,0 +1,13 @@ +use serde::Deserialize; + +use super::ObjectRestSpreadOptions; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2018Options { + #[serde(skip)] + pub object_rest_spread: Option, + + #[serde(skip)] + pub async_generator_functions: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/es2019/mod.rs b/crates/swc_ecma_transformer/oxc/es2019/mod.rs new file mode 100644 index 000000000000..9de620d677cb --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2019/mod.rs @@ -0,0 +1,31 @@ +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{context::TraverseCtx, state::TransformState}; + +mod optional_catch_binding; +mod options; + +pub use optional_catch_binding::OptionalCatchBinding; +pub use options::ES2019Options; + +pub struct ES2019 { + options: ES2019Options, + + // Plugins + optional_catch_binding: OptionalCatchBinding, +} + +impl ES2019 { + pub fn new(options: ES2019Options) -> Self { + Self { optional_catch_binding: OptionalCatchBinding::new(), options } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2019 { + fn enter_catch_clause(&mut self, clause: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.optional_catch_binding { + self.optional_catch_binding.enter_catch_clause(clause, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2019/optional_catch_binding.rs b/crates/swc_ecma_transformer/oxc/es2019/optional_catch_binding.rs new file mode 100644 index 000000000000..150fa3cda41b --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2019/optional_catch_binding.rs @@ -0,0 +1,67 @@ +//! ES2019: Optional Catch Binding +//! +//! This plugin transforms catch clause without parameter to add a parameter +//! called `unused` in catch clause. +//! +//! > This plugin is included in `preset-env`, in ES2019 +//! +//! ## Example +//! +//! Input: +//! ```js +//! try { +//! throw 0; +//! } catch { +//! doSomethingWhichDoesNotCareAboutTheValueThrown(); +//! } +//! ``` +//! +//! Output: +//! ```js +//! try { +//! throw 0; +//! } catch (_unused) { +//! doSomethingWhichDoesNotCareAboutTheValueThrown(); +//! } +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-optional-catch-binding](https://babel.dev/docs/babel-plugin-transform-optional-catch-binding). +//! +//! ## References: +//! * Babel plugin implementation: +//! * Optional catch binding TC39 proposal: + +use oxc_ast::ast::*; +use oxc_semantic::SymbolFlags; +use oxc_span::SPAN; +use oxc_traverse::Traverse; + +use crate::{context::TraverseCtx, state::TransformState}; + +pub struct OptionalCatchBinding; + +impl OptionalCatchBinding { + pub fn new() -> Self { + Self + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for OptionalCatchBinding { + /// If CatchClause has no param, add a parameter called `unused`. + fn enter_catch_clause(&mut self, clause: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) { + if clause.param.is_some() { + return; + } + + let binding = ctx.generate_uid( + "unused", + clause.body.scope_id(), + SymbolFlags::CatchVariable | SymbolFlags::FunctionScopedVariable, + ); + let binding_pattern = binding.create_binding_pattern(ctx); + let param = ctx.ast.catch_parameter(SPAN, binding_pattern); + clause.param = Some(param); + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2019/options.rs b/crates/swc_ecma_transformer/oxc/es2019/options.rs new file mode 100644 index 000000000000..fc6ece2cc019 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2019/options.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2019Options { + #[serde(skip)] + pub optional_catch_binding: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/es2020/mod.rs b/crates/swc_ecma_transformer/oxc/es2020/mod.rs new file mode 100644 index 000000000000..fb4428e9fde8 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2020/mod.rs @@ -0,0 +1,142 @@ +use oxc_ast::ast::*; +use oxc_diagnostics::OxcDiagnostic; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +mod export_namespace_from; +mod nullish_coalescing_operator; +mod optional_chaining; +mod options; +use export_namespace_from::ExportNamespaceFrom; +use nullish_coalescing_operator::NullishCoalescingOperator; +pub use optional_chaining::OptionalChaining; +pub use options::ES2020Options; + +pub struct ES2020<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + options: ES2020Options, + + // Plugins + export_namespace_from: ExportNamespaceFrom<'a, 'ctx>, + nullish_coalescing_operator: NullishCoalescingOperator<'a, 'ctx>, + optional_chaining: OptionalChaining<'a, 'ctx>, +} + +impl<'a, 'ctx> ES2020<'a, 'ctx> { + pub fn new(options: ES2020Options, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + ctx, + options, + export_namespace_from: ExportNamespaceFrom::new(ctx), + nullish_coalescing_operator: NullishCoalescingOperator::new(ctx), + optional_chaining: OptionalChaining::new(ctx), + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2020<'a, '_> { + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.export_namespace_from { + self.export_namespace_from.exit_program(program, ctx); + } + } + + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.nullish_coalescing_operator { + self.nullish_coalescing_operator.enter_expression(expr, ctx); + } + + if self.options.optional_chaining { + self.optional_chaining.enter_expression(expr, ctx); + } + } + + fn enter_formal_parameters( + &mut self, + node: &mut FormalParameters<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.optional_chaining { + self.optional_chaining.enter_formal_parameters(node, ctx); + } + } + + fn exit_formal_parameters( + &mut self, + node: &mut FormalParameters<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.optional_chaining { + self.optional_chaining.exit_formal_parameters(node, ctx); + } + } + + fn enter_big_int_literal(&mut self, node: &mut BigIntLiteral<'a>, _ctx: &mut TraverseCtx<'a>) { + if self.options.big_int { + let warning = OxcDiagnostic::warn( + "Big integer literals are not available in the configured target environment.", + ) + .with_label(node.span); + self.ctx.error(warning); + } + } + + fn enter_import_specifier( + &mut self, + node: &mut ImportSpecifier<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.options.arbitrary_module_namespace_names + && let ModuleExportName::StringLiteral(literal) = &node.imported + { + let warning = OxcDiagnostic::warn( + "Arbitrary module namespace identifier names are not available in the configured target environment.", + ) + .with_label(literal.span); + self.ctx.error(warning); + } + } + + fn enter_export_specifier( + &mut self, + node: &mut ExportSpecifier<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.options.arbitrary_module_namespace_names { + if let ModuleExportName::StringLiteral(literal) = &node.exported { + let warning = OxcDiagnostic::warn( + "Arbitrary module namespace identifier names are not available in the configured target environment.", + ) + .with_label(literal.span); + self.ctx.error(warning); + } + if let ModuleExportName::StringLiteral(literal) = &node.local { + let warning = OxcDiagnostic::warn( + "Arbitrary module namespace identifier names are not available in the configured target environment.", + ) + .with_label(literal.span); + self.ctx.error(warning); + } + } + } + + fn enter_export_all_declaration( + &mut self, + node: &mut ExportAllDeclaration<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.options.arbitrary_module_namespace_names + && let Some(ModuleExportName::StringLiteral(literal)) = &node.exported + { + let warning = OxcDiagnostic::warn( + "Arbitrary module namespace identifier names are not available in the configured target environment.", + ) + .with_label(literal.span); + self.ctx.error(warning); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2020/nullish_coalescing_operator.rs b/crates/swc_ecma_transformer/oxc/es2020/nullish_coalescing_operator.rs new file mode 100644 index 000000000000..270d8455b157 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2020/nullish_coalescing_operator.rs @@ -0,0 +1,221 @@ +//! ES2020: Nullish Coalescing Operator +//! +//! This plugin transforms nullish coalescing operators (`??`) to a series of ternary expressions. +//! +//! > This plugin is included in `preset-env`, in ES2020 +//! +//! ## Example +//! +//! Input: +//! ```js +//! var foo = object.foo ?? "default"; +//! ``` +//! +//! Output: +//! ```js +//! var _object$foo; +//! var foo = +//! (_object$foo = object.foo) !== null && _object$foo !== void 0 +//! ? _object$foo +//! : "default"; +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-nullish-coalescing-operator](https://babeljs.io/docs/babel-plugin-transform-nullish-coalescing-operator). +//! +//! ## References: +//! * Babel plugin implementation: +//! * Nullish coalescing TC39 proposal: + +use oxc_allocator::{Box as ArenaBox, TakeIn}; +use oxc_ast::{NONE, ast::*}; +use oxc_semantic::{ScopeFlags, SymbolFlags}; +use oxc_span::SPAN; +use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator}; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct NullishCoalescingOperator<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> NullishCoalescingOperator<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for NullishCoalescingOperator<'a, '_> { + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + // left ?? right + if !matches!(expr, Expression::LogicalExpression(logical_expr) if logical_expr.operator == LogicalOperator::Coalesce) + { + return; + } + + // Take ownership of the `LogicalExpression` + let Expression::LogicalExpression(logical_expr) = expr.take_in(ctx.ast) else { + unreachable!() + }; + + *expr = self.transform_logical_expression(logical_expr, ctx); + } +} + +impl<'a> NullishCoalescingOperator<'a, '_> { + fn transform_logical_expression( + &self, + logical_expr: ArenaBox<'a, LogicalExpression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let logical_expr = logical_expr.unbox(); + + // Skip creating extra reference when `left` is static + match &logical_expr.left { + Expression::ThisExpression(this) => { + let this_span = this.span; + return Self::create_conditional_expression( + logical_expr.left, + ctx.ast.expression_this(this_span), + ctx.ast.expression_this(this_span), + logical_expr.right, + logical_expr.span, + ctx, + ); + } + Expression::Identifier(ident) => { + let symbol_id = ctx.scoping().get_reference(ident.reference_id()).symbol_id(); + if let Some(symbol_id) = symbol_id { + // Check binding is not mutated. + // TODO(improve-on-babel): Remove this check. Whether binding is mutated or not is not relevant. + if ctx.scoping().get_resolved_references(symbol_id).all(|r| !r.is_write()) { + let binding = BoundIdentifier::new(ident.name, symbol_id); + let ident_span = ident.span; + return Self::create_conditional_expression( + logical_expr.left, + binding.create_spanned_read_expression(ident_span, ctx), + binding.create_spanned_read_expression(ident_span, ctx), + logical_expr.right, + logical_expr.span, + ctx, + ); + } + } + } + _ => {} + } + + // ctx.ancestor(0) is AssignmentPattern + // ctx.ancestor(1) is BindingPattern + // ctx.ancestor(2) is FormalParameter + let is_parent_formal_parameter = + matches!(ctx.ancestor(2), Ancestor::FormalParameterPattern(_)); + + let current_scope_id = if is_parent_formal_parameter { + ctx.create_child_scope_of_current(ScopeFlags::Arrow | ScopeFlags::Function) + } else { + ctx.current_hoist_scope_id() + }; + + // Add `var _name` to scope + let binding = ctx.generate_uid_based_on_node( + &logical_expr.left, + current_scope_id, + SymbolFlags::FunctionScopedVariable, + ); + + let assignment = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + binding.create_write_target(ctx), + logical_expr.left, + ); + let mut new_expr = Self::create_conditional_expression( + assignment, + binding.create_read_expression(ctx), + binding.create_read_expression(ctx), + logical_expr.right, + logical_expr.span, + ctx, + ); + + if is_parent_formal_parameter { + // Replace `function (a, x = a.b ?? c) {}` to `function (a, x = (() => a.b ?? c)() ){}` + // so the temporary variable can be injected in correct scope + let id = binding.create_binding_pattern(ctx); + let param = ctx.ast.formal_parameter(SPAN, ctx.ast.vec(), id, None, false, false); + let params = ctx.ast.formal_parameters( + SPAN, + FormalParameterKind::ArrowFormalParameters, + ctx.ast.vec1(param), + NONE, + ); + let body = ctx.ast.function_body( + SPAN, + ctx.ast.vec(), + ctx.ast.vec1(ctx.ast.statement_expression(SPAN, new_expr)), + ); + let arrow_function = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife( + SPAN, + true, + false, + NONE, + params, + NONE, + body, + current_scope_id, + false, + false, + ); + // `(x) => x;` -> `((x) => x)();` + new_expr = ctx.ast.expression_call(SPAN, arrow_function, NONE, ctx.ast.vec(), false); + } else { + self.ctx.var_declarations.insert_var(&binding, ctx); + } + + new_expr + } + + /// Create a conditional expression. + /// + /// ```js + /// // Input + /// foo = bar ?? "qux" + /// + /// // Output + /// foo = bar !== null && bar !== void 0 ? bar : "qux" + /// // ^^^ assignment ^^^ reference1 ^^^^^ default + /// // ^^^ reference2 + /// ``` + /// + /// ```js + /// // Input + /// foo = bar.x ?? "qux" + /// + /// // Output + /// foo = (_bar$x = bar.x) !== null && _bar$x !== void 0 ? _bar$x : "qux" + /// // ^^^^^^^^^^^^^^^^ assignment ^^^^^^ reference1 ^^^^^ default + /// // ^^^^^^ reference2 + /// ``` + fn create_conditional_expression( + assignment: Expression<'a>, + reference1: Expression<'a>, + reference2: Expression<'a>, + default: Expression<'a>, + span: Span, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let op = BinaryOperator::StrictInequality; + let null = ctx.ast.expression_null_literal(SPAN); + let left = ctx.ast.expression_binary(SPAN, assignment, op, null); + let right = ctx.ast.expression_binary(SPAN, reference1, op, ctx.ast.void_0(SPAN)); + let test = ctx.ast.expression_logical(SPAN, left, LogicalOperator::And, right); + + ctx.ast.expression_conditional(span, test, reference2, default) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2020/optional_chaining.rs b/crates/swc_ecma_transformer/oxc/es2020/optional_chaining.rs new file mode 100644 index 000000000000..5538169a1796 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2020/optional_chaining.rs @@ -0,0 +1,665 @@ +//! ES2020: Optional Chaining +//! +//! This plugin transforms [`ChainExpression`] into a series of `null` and `void 0` checks, +//! resulting in a conditional expression. +//! +//! > This plugin is included in `preset-env`, in ES2020. +//! +//! ## Example +//! +//! Input: +//! ```js +//! const foo = {}; +//! // Read +//! foo?.bar?.bar; +//! // Call +//! foo?.bar?.baz?.(); +//! // Delete +//! delete foo?.bar?.baz; +//! ``` +//! +//! Output: +//! ```js +//! var _foo$bar, _foo$bar2, _foo$bar2$baz, _foo$bar3; +//! const foo = {}; +//! // Read +//! foo === null || foo === void 0 || (_foo$bar = foo.bar) === null || +//! _foo$bar === void 0 ? void 0 : _foo$bar.bar; +//! // Call +//! foo === null || foo === void 0 || (_foo$bar2 = foo.bar) === null || +//! _foo$bar2 === void 0 || (_foo$bar2$baz = _foo$bar2.baz) === null || +//! _foo$bar2$baz === void 0 ? void 0 : _foo$bar2$baz.call(_foo$bar2); +//! // Delete +//! foo === null || foo === void 0 || (_foo$bar3 = foo.bar) === null || +//! _foo$bar3 === void 0 ? true : delete _foo$bar3.baz; +//! ``` +//! +//! ## Implementation +//! +//! Due to the different architecture, we found it hard to port the implementation from Babel directly; +//! however, our implementation is written based on Babel’s transformed output. +//! +//! Nevertheless, our outputs still have some differences from Babel’s output. +//! +//! ## References +//! +//! * Babel docs: +//! * Babel implementation: +//! * Optional chaining TC39 proposal: + +use std::mem; + +use oxc_allocator::{CloneIn, TakeIn}; +use oxc_ast::{NONE, ast::*}; +use oxc_span::SPAN; +use oxc_traverse::{Ancestor, BoundIdentifier, MaybeBoundIdentifier, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, + utils::ast_builder::wrap_expression_in_arrow_function_iife, +}; + +#[derive(Debug)] +enum CallContext<'a> { + /// `new.target?.()` + None, + /// `super.method?.()` + This, + /// All other cases + Binding(MaybeBoundIdentifier<'a>), +} + +pub struct OptionalChaining<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + + // states + is_inside_function_parameter: bool, + temp_binding: Option>, + /// .call(context) + /// ^^^^^^^ + call_context: CallContext<'a>, +} + +impl<'a, 'ctx> OptionalChaining<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + ctx, + is_inside_function_parameter: false, + temp_binding: None, + call_context: CallContext::None, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for OptionalChaining<'a, '_> { + // `#[inline]` because this is a hot path + #[inline] + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + match expr { + Expression::ChainExpression(_) => self.transform_chain_expression(expr, ctx), + Expression::UnaryExpression(unary_expr) + if unary_expr.operator == UnaryOperator::Delete + && matches!(unary_expr.argument, Expression::ChainExpression(_)) => + { + self.transform_update_expression(expr, ctx); + } + _ => {} + } + } + + // `#[inline]` because this is a hot path + #[inline] + fn enter_formal_parameters(&mut self, _: &mut FormalParameters<'a>, _: &mut TraverseCtx<'a>) { + self.is_inside_function_parameter = true; + } + + // `#[inline]` because this is a hot path + #[inline] + fn exit_formal_parameters( + &mut self, + _node: &mut FormalParameters<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + self.is_inside_function_parameter = false; + } +} + +impl<'a> OptionalChaining<'a, '_> { + fn set_temp_binding(&mut self, binding: BoundIdentifier<'a>) { + self.temp_binding.replace(binding); + } + + fn set_binding_context(&mut self, binding: MaybeBoundIdentifier<'a>) { + self.call_context = CallContext::Binding(binding); + } + + fn set_this_context(&mut self) { + self.call_context = CallContext::This; + } + + /// Get the call context from [`Self::call_context`] + fn get_call_context(&self, ctx: &mut TraverseCtx<'a>) -> Argument<'a> { + debug_assert!(!matches!(self.call_context, CallContext::None)); + Argument::from(if let CallContext::Binding(binding) = &self.call_context { + binding.create_read_expression(ctx) + } else { + ctx.ast.expression_this(SPAN) + }) + } + + /// Given an IdentifierReference which is [`CallExpression::callee`] to compare with the collected context + fn should_specify_context( + &self, + ident: &IdentifierReference<'a>, + ctx: &TraverseCtx<'a>, + ) -> bool { + match &self.call_context { + CallContext::None => false, + CallContext::This => true, + CallContext::Binding(binding) => { + binding.name != ident.name + || binding.symbol_id.is_some_and(|symbol_id| { + ctx.scoping() + .get_reference(ident.reference_id()) + .symbol_id() + .is_some_and(|id| id != symbol_id) + }) + } + } + } + + /// Check if we should create a temp variable for the identifier. + /// + /// Except for `eval`, we should create a temp variable for all global references. + /// + /// If no temp variable required, returns `MaybeBoundIdentifier` for existing variable/global. + /// If temp variable is required, returns `None`. + fn get_existing_binding_for_identifier( + &self, + ident: &IdentifierReference<'a>, + ctx: &TraverseCtx<'a>, + ) -> Option> { + let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx); + if self.ctx.assumptions.pure_getters + || binding.to_bound_identifier().is_some() + || ident.name == "eval" + { + Some(binding) + } else { + None + } + } + + /// Return `left === null` + fn wrap_null_check(&self, left: Expression<'a>, ctx: &TraverseCtx<'a>) -> Expression<'a> { + let operator = if self.ctx.assumptions.no_document_all { + BinaryOperator::Equality + } else { + BinaryOperator::StrictEquality + }; + ctx.ast.expression_binary(SPAN, left, operator, ctx.ast.expression_null_literal(SPAN)) + } + + /// Return `left === void 0` + fn wrap_void0_check(left: Expression<'a>, ctx: &TraverseCtx<'a>) -> Expression<'a> { + let operator = BinaryOperator::StrictEquality; + ctx.ast.expression_binary(SPAN, left, operator, ctx.ast.void_0(SPAN)) + } + + /// Return `left1 === null || left2 === void 0` + fn wrap_optional_check( + &self, + left1: Expression<'a>, + left2: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let null_check = self.wrap_null_check(left1, ctx); + let void0_check = Self::wrap_void0_check(left2, ctx); + Self::create_logical_expression(null_check, void0_check, ctx) + } + + /// Return `left || right` + fn create_logical_expression( + left: Expression<'a>, + right: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + ctx.ast.expression_logical(SPAN, left, LogicalOperator::Or, right) + } + + /// Return `left ? void 0 : alternative` + /// + /// The [`ConditionalExpression::consequent`] depends on whether + /// `is_delete` is true or false. + fn create_conditional_expression( + is_delete: bool, + test: Expression<'a>, + alternate: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let consequent = if is_delete { + ctx.ast.expression_boolean_literal(SPAN, true) + } else { + ctx.ast.void_0(SPAN) + }; + ctx.ast.expression_conditional(SPAN, test, consequent, alternate) + } + + /// Convert chain expression to expression + /// + /// - [ChainElement::CallExpression] -> [Expression::CallExpression] + /// - [ChainElement::StaticMemberExpression] -> [Expression::StaticMemberExpression] + /// - [ChainElement::ComputedMemberExpression] -> [Expression::ComputedMemberExpression] + /// - [ChainElement::PrivateFieldExpression] -> [Expression::PrivateFieldExpression] + /// - [ChainElement::TSNonNullExpression] -> [TSNonNullExpression::expression] + /// + /// `#[inline]` so that compiler sees that `expr` is an [`Expression::ChainExpression`]. + #[inline] + fn convert_chain_expression_to_expression( + expr: &mut Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let Expression::ChainExpression(chain_expr) = expr.take_in(ctx.ast) else { unreachable!() }; + match chain_expr.unbox().expression { + element @ match_member_expression!(ChainElement) => { + Expression::from(element.into_member_expression()) + } + ChainElement::CallExpression(call) => Expression::CallExpression(call), + ChainElement::TSNonNullExpression(non_null) => non_null.unbox().expression, + } + } + + /// Return `left = right` + fn create_assignment_expression( + left: AssignmentTarget<'a>, + right: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, left, right) + } + + /// Transform chain expression + fn transform_chain_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + *expr = if self.is_inside_function_parameter { + // To insert the temp binding in the correct scope, we wrap the expression with + // an arrow function. During the chain expression transformation, the temp binding + // will be inserted into the arrow function's body. + wrap_expression_in_arrow_function_iife(expr.take_in(ctx.ast), ctx) + } else { + self.transform_chain_expression_impl(false, expr, ctx) + } + } + + /// Transform update expression + fn transform_update_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + *expr = if self.is_inside_function_parameter { + // Same as the above `transform_chain_expression` explanation + wrap_expression_in_arrow_function_iife(expr.take_in(ctx.ast), ctx) + } else { + // Unfortunately no way to get compiler to see that this branch is provably unreachable. + // We don't want to inline this function, to keep `enter_expression` as small as possible. + let Expression::UnaryExpression(unary_expr) = expr else { unreachable!() }; + self.transform_chain_expression_impl(true, &mut unary_expr.argument, ctx) + } + } + + /// Transform chain expression to conditional expression which contains a lot of checks + /// + /// This is the root transform function for chain expressions. It calls + /// [`Self::transform_chain_element_recursion`] to transform the chain expression elements, + /// and then joins the transformed elements with the conditional expression. + fn transform_chain_expression_impl( + &mut self, + is_delete: bool, + chain_expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let mut chain_expr = Self::convert_chain_expression_to_expression(chain_expr, ctx); + // ^^^^^^^^^^ After the recursive transformation, the chain_expr will be transformed into + // a pure non-optional expression and it's the last part of the chain expression. + + let left = + self.transform_chain_element_recursion(&mut chain_expr, ctx).unwrap_or_else(|| { + unreachable!( + "Given chain expression certainly contains at least one optional expression, + so it must return a transformed expression" + ) + }); + + // If the chain expression is an argument of a UnaryExpression and its operator is `delete`, + // we need to wrap the last part with a `delete` unary expression + // `delete foo?.bar` -> `... || delete _Foo.bar;` + // ^^^^^^ ^^^^^^^^ Here we will wrap the right part with a `delete` unary expression + if is_delete { + chain_expr = ctx.ast.expression_unary(SPAN, UnaryOperator::Delete, chain_expr); + } + + // If this chain expression is a callee of a CallExpression, we need to transform it to accept a proper context + // `(Foo?.["m"])();` -> `(... _Foo["m"].bind(_Foo))();` + // ^^^^^^^^^^^ Here we will handle the `right` part to bind a proper context + if ctx.parent().is_parenthesized_expression() + && matches!(ctx.ancestor(1), Ancestor::CallExpressionCallee(_)) + { + chain_expr = self.transform_expression_to_bind_context(chain_expr, ctx); + } + // Clear states + self.temp_binding = None; + self.call_context = CallContext::None; + + Self::create_conditional_expression(is_delete, left, chain_expr, ctx) + } + + /// Transform an expression to bind a proper context + /// + /// `Foo.bar` -> `Foo.bar.bind(context)` + fn transform_expression_to_bind_context( + &self, + mut expr: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // Find proper context + let context = if let Some(member) = expr.as_member_expression_mut() { + let object = member.object_mut().get_inner_expression_mut(); + let context = if self.ctx.assumptions.pure_getters { + // TODO: `clone_in` causes reference loss of reference id + object.clone_in(ctx.ast.allocator) + } else if let Expression::Identifier(ident) = object { + MaybeBoundIdentifier::from_identifier_reference(ident, ctx) + .create_read_expression(ctx) + } else { + // `foo.bar` -> `_foo$bar = foo.bar` + let binding = self.ctx.var_declarations.create_uid_var_based_on_node(object, ctx); + *object = Self::create_assignment_expression( + binding.create_write_target(ctx), + object.take_in(ctx.ast), + ctx, + ); + binding.create_read_expression(ctx) + }; + Argument::from(context) + } else { + self.get_call_context(ctx) + }; + + // `expr.bind(context)` + let arguments = ctx.ast.vec1(context); + let property = ctx.ast.identifier_name(SPAN, "bind"); + let callee = ctx.ast.member_expression_static(SPAN, expr, property, false); + let callee = Expression::from(callee); + ctx.ast.expression_call(SPAN, callee, NONE, arguments, false) + } + + /// Recursively transform chain expression elements + /// + /// ## Depth-first transformation + /// + /// Start from the given [`Expression`] which is converted from [`ChainExpression::expression`] + /// by [`Self::convert_chain_expression_to_expression`], and dive into the deepest + /// expression, until it reaches the end of the chain expression and starts to transform. + /// + /// ### Demonstration + /// + /// For the given expression `foo?.bar?.baz?.()` + /// + /// > NOTE: Here assume that `foo` is defined somewhere. + /// + /// 1. Start from the root expression `foo?.bar?.baz?.()` + /// + /// 2. Recurse and go into the deepest optional expression `foo?.bar` + /// + /// 3. The `foo?.bar` is an optional [`StaticMemberExpression`], so transform `foo` to + /// `foo === null || foo === void 0` and return the transformed expression back to the parent + /// + /// 4. Got to here, we now have a left expression as the above transformed expression, and the current expression + /// is `foo.bar?.baz`, and it's also an optional [`StaticMemberExpression`], so transform `foo.bar` to + /// `(_foo$bar = foo.bar) === null || _foo$bar === void 0` and join it with the left expression, and return + /// the joined expression back to the parent. + /// + /// > NOTE: The callee(`foo.bar`) is assigned to a temp binding(`_foo$bar`), so the original callee is also replaced with + /// > the temp binding(`_foo$bar`) + /// + /// 5. Repeat the above steps until back to the root expression, and the final expression will be + /// + /// ```js + /// foo === null || foo === void 0 || (_foo$bar = foo.bar) === null || _foo$bar === void 0 || + /// (_foo$bar$baz = _foo$bar.baz) === null || _foo$bar$baz === void 0; + /// ``` + /// + /// After transformation, the passed-in expression will be replaced with `_foo$bar$baz.call(_foo$bar)`, + /// and it will be used to construct the final conditional expression in [`Self::transform_chain_expression`] + fn transform_chain_element_recursion( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // Skip parenthesized expression or other TS-syntax expressions + let expr = expr.get_inner_expression_mut(); + match expr { + // `(foo?.bar)?.baz` + // ^^^^^^^^^^ The nested chain expression is always under the ParenthesizedExpression + Expression::ChainExpression(_) => { + *expr = Self::convert_chain_expression_to_expression(expr, ctx); + self.transform_chain_element_recursion(expr, ctx) + } + // `foo?.bar?.baz` + Expression::StaticMemberExpression(member) => { + let left = self.transform_chain_element_recursion(&mut member.object, ctx); + if member.optional { + member.optional = false; + Some(self.transform_optional_expression(false, left, &mut member.object, ctx)) + } else { + left + } + } + // `foo?.[bar]?.[baz]` + Expression::ComputedMemberExpression(member) => { + let left = self.transform_chain_element_recursion(&mut member.object, ctx); + if member.optional { + member.optional = false; + Some(self.transform_optional_expression(false, left, &mut member.object, ctx)) + } else { + left + } + } + // `this?.#foo?.bar` + Expression::PrivateFieldExpression(member) => { + let left = self.transform_chain_element_recursion(&mut member.object, ctx); + if member.optional { + member.optional = false; + Some(self.transform_optional_expression(false, left, &mut member.object, ctx)) + } else { + left + } + } + // `foo?.bar?.bar?.()` + Expression::CallExpression(call) => { + let left = self.transform_chain_element_recursion(&mut call.callee, ctx); + if call.optional { + call.optional = false; + let callee = call.callee.get_inner_expression_mut(); + let left = Some(self.transform_optional_expression(true, left, callee, ctx)); + + if !self.ctx.assumptions.pure_getters { + // After transformation of the callee, this call expression may lose the original context, + // so we need to check if we need to specify the context. + if let Expression::Identifier(ident) = callee + && self.should_specify_context(ident, ctx) + { + // `foo$bar(...)` -> `foo$bar.call(context, ...)` + let callee = callee.take_in(ctx.ast); + let property = ctx.ast.identifier_name(SPAN, "call"); + let member = + ctx.ast.member_expression_static(SPAN, callee, property, false); + call.callee = Expression::from(member); + call.arguments.insert(0, self.get_call_context(ctx)); + } + } + + left + } else { + left + } + } + _ => None, + } + } + + /// Transform optional expressions + /// + /// - `foo` -> `foo === null || foo === void 0` + /// - `foo.bar` -> `(foo$bar = foo.bar) === null || foo$bar === void 0` + /// + /// NOTE: After transformation, the original expression will be replaced with the temp binding + fn transform_optional_expression( + &mut self, + is_call: bool, + left: Option>, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if let Some(left) = left { + return self.transform_and_join_expression(is_call, left, expr, ctx); + } + + // Skip parenthesized expression or other TS-syntax expressions + let expr = expr.get_inner_expression_mut(); + + // If the expression is an identifier and it's not a global reference, we just wrap it with checks + // `foo` -> `foo === null || foo === void 0` + if let Expression::Identifier(ident) = expr + && let Some(binding) = self.get_existing_binding_for_identifier(ident, ctx) + { + if ident.name == "eval" { + // `eval?.()` is an indirect eval call transformed to `(0,eval)()` + let zero = ctx.ast.number_0(); + let original_callee = expr.take_in(ctx.ast); + let expressions = ctx.ast.vec_from_array([zero, original_callee]); + *expr = ctx.ast.expression_sequence(SPAN, expressions); + } + + let left1 = binding.create_read_expression(ctx); + let replacement = if self.ctx.assumptions.no_document_all { + // `foo === null` + self.wrap_null_check(left1, ctx) + } else { + // `foo === null || foo === void 0` + let left2 = binding.create_read_expression(ctx); + self.wrap_optional_check(left1, left2, ctx) + }; + self.set_binding_context(binding); + return replacement; + } + + // We should generate a temp binding for the expression first to avoid the next step changing the expression. + let temp_binding = self.ctx.var_declarations.create_uid_var_based_on_node(expr, ctx); + if is_call && !self.ctx.assumptions.pure_getters { + self.set_chain_call_context(expr, ctx); + } + + // Replace the expression with the temp binding and assign the original expression to the temp binding + let expr = mem::replace(expr, temp_binding.create_read_expression(ctx)); + // `(binding = expr)` + let assignment_expression = + Self::create_assignment_expression(temp_binding.create_write_target(ctx), expr, ctx); + let expr = if self.ctx.assumptions.no_document_all { + // `(binding = expr) === null` + self.wrap_null_check(assignment_expression, ctx) + } else { + // `(binding = expr) === null || binding === void 0` + self.wrap_optional_check( + assignment_expression, + temp_binding.create_read_expression(ctx), + ctx, + ) + }; + + self.set_temp_binding(temp_binding); + expr + } + + /// Transform the expression and join it with the `left` expression + /// + /// - `left || (binding = expr) === null || binding === void 0` + /// + /// NOTE: After transformation, the original expression will be replaced with the temp binding + fn transform_and_join_expression( + &mut self, + is_call: bool, + left: Expression<'a>, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if is_call { + // We cannot reuse the temp binding for calls because we need to + // store both the method and the receiver. + // And because we will create a new temp binding for the callee and the original temp binding + // will become the call context, we take the current temp binding and set it + // as the call context. + if let Some(temp_binding) = self.temp_binding.take() { + self.set_binding_context(temp_binding.to_maybe_bound_identifier()); + } + self.set_chain_call_context(expr, ctx); + } + + let temp_binding = { + if self.temp_binding.is_none() { + let binding = self.ctx.var_declarations.create_uid_var_based_on_node(expr, ctx); + self.set_temp_binding(binding); + } + self.temp_binding.as_ref().unwrap() + }; + + // Replace the expression with the temp binding and assign the original expression to the temp binding + let expr = mem::replace(expr, temp_binding.create_read_expression(ctx)); + // `(binding = expr)` + let assignment_expression = + Self::create_assignment_expression(temp_binding.create_write_target(ctx), expr, ctx); + + // `left || (binding = expr) === null` + let left = Self::create_logical_expression( + left, + self.wrap_null_check(assignment_expression, ctx), + ctx, + ); + + if self.ctx.assumptions.no_document_all { + left + } else { + let reference = temp_binding.create_read_expression(ctx); + // `left || (binding = expr) === null || binding === void 0` + Self::create_logical_expression(left, Self::wrap_void0_check(reference, ctx), ctx) + } + } + + fn set_chain_call_context(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(member) = expr.as_member_expression_mut() { + let object = member.object_mut(); + // If the [`MemberExpression::object`] is a global reference, we need to assign it to a temp binding. + // i.e `foo` -> `(_foo = foo)` + if matches!(object, Expression::Super(_) | Expression::ThisExpression(_)) { + self.set_this_context(); + } else { + let binding = object + .get_identifier_reference() + .and_then(|ident| self.get_existing_binding_for_identifier(ident, ctx)) + .unwrap_or_else(|| { + let binding = + self.ctx.var_declarations.create_uid_var_based_on_node(object, ctx); + // `(_foo = foo)` + *object = Self::create_assignment_expression( + binding.create_write_target(ctx), + object.take_in(ctx.ast), + ctx, + ); + binding.to_maybe_bound_identifier() + }); + self.set_binding_context(binding); + } + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2020/options.rs b/crates/swc_ecma_transformer/oxc/es2020/options.rs new file mode 100644 index 000000000000..961f76dc7db2 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2020/options.rs @@ -0,0 +1,20 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2020Options { + #[serde(skip)] + pub export_namespace_from: bool, + + #[serde(skip)] + pub nullish_coalescing_operator: bool, + + #[serde(skip)] + pub big_int: bool, + + #[serde(skip)] + pub optional_chaining: bool, + + #[serde(skip)] + pub arbitrary_module_namespace_names: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/es2021/logical_assignment_operators.rs b/crates/swc_ecma_transformer/oxc/es2021/logical_assignment_operators.rs new file mode 100644 index 000000000000..455bd576c67d --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2021/logical_assignment_operators.rs @@ -0,0 +1,207 @@ +//! ES2021: Logical Assignment Operators +//! +//! This plugin transforms logical assignment operators (`&&=`, `||=`, `??=`) +//! to a series of logical expressions. +//! +//! > This plugin is included in `preset-env`, in ES2021 +//! +//! ## Example +//! +//! Input: +//! ```js +//! a ||= b; +//! obj.a.b ||= c; +//! +//! a &&= b; +//! obj.a.b &&= c; +//! ``` +//! +//! Output: +//! ```js +//! var _obj$a, _obj$a2; +//! +//! a || (a = b); +//! (_obj$a = obj.a).b || (_obj$a.b = c); +//! +//! a && (a = b); +//! (_obj$a2 = obj.a).b && (_obj$a2.b = c); +//! ``` +//! +//! ### With Nullish Coalescing +//! +//! > While using the [nullish-coalescing-operator](https://github.com/oxc-project/oxc/blob/main/crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs) plugin (included in `preset-env``) +//! +//! Input: +//! ```js +//! a ??= b; +//! obj.a.b ??= c; +//! ``` +//! +//! Output: +//! ```js +//! var _a, _obj$a, _obj$a$b; +//! +//! (_a = a) !== null && _a !== void 0 ? _a : (a = b); +//! (_obj$a$b = (_obj$a = obj.a).b) !== null && _obj$a$b !== void 0 +//! ? _obj$a$b +//! : (_obj$a.b = c); +//! ``` +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-logical-assignment-operators](https://babel.dev/docs/babel-plugin-transform-logical-assignment-operators). +//! +//! ## References: +//! * Babel plugin implementation: +//! * Logical Assignment TC39 proposal: + +use oxc_allocator::TakeIn; +use oxc_ast::ast::*; +use oxc_semantic::ReferenceFlags; +use oxc_span::SPAN; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct LogicalAssignmentOperators<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> LogicalAssignmentOperators<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for LogicalAssignmentOperators<'a, '_> { + // `#[inline]` because this is a hot path, and most `Expression`s are not `AssignmentExpression`s + // with a logical operator. So we want to bail out as fast as possible for everything else, + // without the cost of a function call. + #[inline] + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let Expression::AssignmentExpression(assignment_expr) = expr else { return }; + + // `&&=` `||=` `??=` + let Some(operator) = assignment_expr.operator.to_logical_operator() else { return }; + + self.transform_logical_assignment(expr, operator, ctx); + } +} + +impl<'a> LogicalAssignmentOperators<'a, '_> { + fn transform_logical_assignment( + &self, + expr: &mut Expression<'a>, + operator: LogicalOperator, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assignment_expr) = expr else { unreachable!() }; + + // `a &&= c` -> `a && (a = c);` + // ^ ^ assign_target + // ^ left_expr + + // TODO: Add tests, cover private identifier + let (left_expr, assign_target) = match &mut assignment_expr.left { + // `a &&= c` -> `a && (a = c)` + AssignmentTarget::AssignmentTargetIdentifier(ident) => { + Self::convert_identifier(ident, ctx) + } + // `a.b &&= c` -> `var _a; (_a = a).b && (_a.b = c)` + AssignmentTarget::StaticMemberExpression(static_expr) => { + self.convert_static_member_expression(static_expr, ctx) + } + // `a[b.y] &&= c;` -> + // `var _a, _b$y; (_a = a)[_b$y = b.y] && (_a[_b$y] = c);` + AssignmentTarget::ComputedMemberExpression(computed_expr) => { + self.convert_computed_member_expression(computed_expr, ctx) + } + // TODO + #[expect(clippy::match_same_arms)] + AssignmentTarget::PrivateFieldExpression(_) => return, + // All other are TypeScript syntax. + + // It is a Syntax Error if AssignmentTargetType of LeftHandSideExpression is not simple. + // So safe to return here. + _ => return, + }; + + let assign_op = AssignmentOperator::Assign; + let right = assignment_expr.right.take_in(ctx.ast); + let right = ctx.ast.expression_assignment(SPAN, assign_op, assign_target, right); + + let logical_expr = ctx.ast.expression_logical(SPAN, left_expr, operator, right); + + *expr = logical_expr; + } + + fn convert_identifier( + ident: &IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, AssignmentTarget<'a>) { + let reference = ctx.scoping_mut().get_reference_mut(ident.reference_id()); + *reference.flags_mut() = ReferenceFlags::Read; + let symbol_id = reference.symbol_id(); + let left_expr = Expression::Identifier(ctx.alloc(ident.clone())); + + let ident = ctx.create_ident_reference(SPAN, ident.name, symbol_id, ReferenceFlags::Write); + let assign_target = AssignmentTarget::AssignmentTargetIdentifier(ctx.alloc(ident)); + (left_expr, assign_target) + } + + fn convert_static_member_expression( + &self, + static_expr: &mut StaticMemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, AssignmentTarget<'a>) { + let object = static_expr.object.take_in(ctx.ast); + let (object, object_ref) = self.ctx.duplicate_expression(object, true, ctx); + + let left_expr = Expression::from(ctx.ast.member_expression_static( + static_expr.span, + object, + static_expr.property.clone(), + false, + )); + + let assign_expr = ctx.ast.member_expression_static( + static_expr.span, + object_ref, + static_expr.property.clone(), + false, + ); + let assign_target = AssignmentTarget::from(assign_expr); + + (left_expr, assign_target) + } + + fn convert_computed_member_expression( + &self, + computed_expr: &mut ComputedMemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, AssignmentTarget<'a>) { + let object = computed_expr.object.take_in(ctx.ast); + let (object, object_ref) = self.ctx.duplicate_expression(object, true, ctx); + + let expression = computed_expr.expression.take_in(ctx.ast); + let (expression, expression_ref) = self.ctx.duplicate_expression(expression, true, ctx); + + let left_expr = Expression::from(ctx.ast.member_expression_computed( + computed_expr.span, + object, + expression, + false, + )); + + let assign_target = AssignmentTarget::from(ctx.ast.member_expression_computed( + computed_expr.span, + object_ref, + expression_ref, + false, + )); + + (left_expr, assign_target) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2021/mod.rs b/crates/swc_ecma_transformer/oxc/es2021/mod.rs new file mode 100644 index 000000000000..b34f935b5e66 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2021/mod.rs @@ -0,0 +1,34 @@ +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +mod logical_assignment_operators; +mod options; + +pub use logical_assignment_operators::LogicalAssignmentOperators; +pub use options::ES2021Options; + +pub struct ES2021<'a, 'ctx> { + options: ES2021Options, + + // Plugins + logical_assignment_operators: LogicalAssignmentOperators<'a, 'ctx>, +} + +impl<'a, 'ctx> ES2021<'a, 'ctx> { + pub fn new(options: ES2021Options, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { logical_assignment_operators: LogicalAssignmentOperators::new(ctx), options } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2021<'a, '_> { + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.logical_assignment_operators { + self.logical_assignment_operators.enter_expression(expr, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2021/options.rs b/crates/swc_ecma_transformer/oxc/es2021/options.rs new file mode 100644 index 000000000000..6d1583dccc3c --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2021/options.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2021Options { + #[serde(skip)] + pub logical_assignment_operators: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/class.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/class.rs new file mode 100644 index 000000000000..77520ee244ef --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/class.rs @@ -0,0 +1,902 @@ +//! ES2022: Class Properties +//! Transform of class itself. + +use indexmap::map::Entry; +use oxc_allocator::{Address, GetAddress, TakeIn}; +use oxc_ast::{NONE, ast::*}; +use oxc_span::SPAN; +use oxc_syntax::{ + node::NodeId, + reference::ReferenceFlags, + scope::ScopeFlags, + symbol::{SymbolFlags, SymbolId}, +}; +use oxc_traverse::{Ancestor, BoundIdentifier}; + +use crate::{ + common::helper_loader::Helper, + context::{TransformCtx, TraverseCtx}, + utils::ast_builder::create_assignment, +}; + +use super::{ + ClassBindings, ClassDetails, ClassProperties, FxIndexMap, PrivateProp, + constructor::InstanceInitsInsertLocation, + utils::{create_variable_declaration, exprs_into_stmts}, +}; + +// TODO(improve-on-babel): If outer scope is sloppy mode, all code which is moved to outside +// the class should be wrapped in an IIFE with `'use strict'` directive. Babel doesn't do this. + +// TODO: If static blocks transform is disabled, it's possible to get incorrect execution order. +// ```js +// class C { +// static x = console.log('x'); +// static { +// console.log('block'); +// } +// static y = console.log('y'); +// } +// ``` +// This logs "x", "block", "y". But in transformed output it'd be "block", "x", "y". +// Maybe force transform of static blocks if any static properties? +// Or alternatively could insert static property initializers into static blocks. + +impl<'a> ClassProperties<'a, '_> { + /// Perform first phase of transformation of class. + /// + /// This is the only entry point into the transform upon entering class body. + /// + /// First we check if any transforms are necessary, and exit if not. + /// + /// If transform is required: + /// * Build a hashmap of private property keys. + /// * Push `ClassDetails` containing info about the class to `classes_stack`. + /// * Extract instance property initializers (public or private) from class body and insert into + /// class constructor. + /// * Temporarily replace computed keys of instance properties with assignments to temp vars. + /// `class C { [foo()] = 123; }` -> `class C { [_foo = foo()]; }` + pub(super) fn transform_class_body_on_entry( + &mut self, + body: &mut ClassBody<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Ignore TS class declarations + // TODO: Is this correct? + let Ancestor::ClassBody(class) = ctx.parent() else { unreachable!() }; + if *class.declare() { + return; + } + + // Get basic details about class + let is_declaration = *class.r#type() == ClassType::ClassDeclaration; + let mut class_name_binding = class.id().as_ref().map(BoundIdentifier::from_binding_ident); + let class_scope_id = class.scope_id().get().unwrap(); + let has_super_class = class.super_class().is_some(); + + // Check if class has any properties, private methods, or static blocks + let mut instance_prop_count = 0; + let mut has_static_prop = false; + let mut has_instance_private_method = false; + let mut has_static_private_method_or_static_block = false; + // TODO: Store `FxIndexMap`s in a pool and re-use them + let mut private_props = FxIndexMap::default(); + for element in &mut body.body { + match element { + ClassElement::PropertyDefinition(prop) => { + // TODO: Throw error if property has decorators + + // Ignore `declare` properties as they don't have any runtime effect, + // and will be removed in the TypeScript transform later + if prop.declare { + continue; + } + + // Create binding for private property key + if let PropertyKey::PrivateIdentifier(ident) = &prop.key { + // Note: Current scope is outside class. + let binding = ctx.generate_uid_in_current_hoist_scope(&ident.name); + private_props.insert( + ident.name, + PrivateProp::new(binding, prop.r#static, None, false), + ); + } + + if prop.r#static { + has_static_prop = true; + } else { + instance_prop_count += 1; + } + } + ClassElement::StaticBlock(_) => { + // Static block only necessitates transforming class if it's being transformed + if self.transform_static_blocks { + has_static_private_method_or_static_block = true; + } + } + ClassElement::MethodDefinition(method) => { + if let PropertyKey::PrivateIdentifier(ident) = &method.key { + if method.r#static { + has_static_private_method_or_static_block = true; + } else { + has_instance_private_method = true; + } + + let name = match method.kind { + MethodDefinitionKind::Method => ident.name.as_str(), + MethodDefinitionKind::Get => &format!("get_{}", ident.name), + MethodDefinitionKind::Set => &format!("set_{}", ident.name), + MethodDefinitionKind::Constructor => unreachable!(), + }; + let binding = ctx.generate_uid( + name, + ctx.current_block_scope_id(), + SymbolFlags::Function, + ); + + match private_props.entry(ident.name) { + Entry::Occupied(mut entry) => { + // If there's already a binding for this private property, + // it's a setter or getter, so store the binding in `binding2`. + entry.get_mut().set_binding2(binding); + } + Entry::Vacant(entry) => { + entry.insert(PrivateProp::new( + binding, + method.r#static, + Some(method.kind), + false, + )); + } + } + } + } + ClassElement::AccessorProperty(prop) => { + // TODO: Not sure what we should do here. + // Only added this to prevent panics in TS conformance tests. + if let PropertyKey::PrivateIdentifier(ident) = &prop.key { + let dummy_binding = BoundIdentifier::new(Atom::empty(), SymbolId::new(0)); + private_props.insert( + ident.name, + PrivateProp::new(dummy_binding, prop.r#static, None, true), + ); + } + } + ClassElement::TSIndexSignature(_) => { + // TODO: Need to handle this? + } + } + } + + self.private_field_count += private_props.len(); + + // Exit if nothing to transform + if instance_prop_count == 0 + && !has_static_prop + && !has_static_private_method_or_static_block + && !has_instance_private_method + { + self.classes_stack.push(ClassDetails { + is_declaration, + is_transform_required: false, + private_props: if private_props.is_empty() { None } else { Some(private_props) }, + bindings: ClassBindings::dummy(), + }); + return; + } + + // Initialize class binding vars. + // Static prop in class expression or anonymous `export default class {}` always require + // temp var for class. Static prop in class declaration doesn't. + let need_temp_var = has_static_prop && (!is_declaration || class_name_binding.is_none()); + + let outer_hoist_scope_id = ctx.current_hoist_scope_id(); + let class_temp_binding = if need_temp_var { + let temp_binding = ClassBindings::create_temp_binding( + class_name_binding.as_ref(), + outer_hoist_scope_id, + ctx, + ); + if is_declaration { + // Anonymous `export default class {}`. Set class name binding to temp var. + // Actual class name will be set to this later. + class_name_binding = Some(temp_binding.clone()); + } else { + // Create temp var `var _Class;` statement. + // TODO(improve-on-babel): Inserting the temp var `var _Class` statement here is only + // to match Babel's output. It'd be simpler just to insert it at the end and get rid of + // `temp_var_is_created` that tracks whether it's done already or not. + self.ctx.var_declarations.insert_var(&temp_binding, ctx); + } + Some(temp_binding) + } else { + None + }; + + let class_brand_binding = has_instance_private_method.then(|| { + // `_Class_brand` + let name = class_name_binding.as_ref().map_or_else(|| "Class", |binding| &binding.name); + let name = &format!("_{name}_brand"); + ctx.generate_uid_in_current_hoist_scope(name) + }); + + let static_private_fields_use_temp = !is_declaration; + let class_bindings = ClassBindings::new( + class_name_binding, + class_temp_binding, + class_brand_binding, + outer_hoist_scope_id, + static_private_fields_use_temp, + need_temp_var, + ); + + // Add entry to `classes_stack` + self.classes_stack.push(ClassDetails { + is_declaration, + is_transform_required: true, + private_props: if private_props.is_empty() { None } else { Some(private_props) }, + bindings: class_bindings, + }); + + // Exit if no instance properties (public or private) + if instance_prop_count == 0 && !has_instance_private_method { + return; + } + + // Extract instance properties initializers. + // + // We leave the properties themselves in place, but take the initializers. + // `class C { prop = 123; }` -> `class { prop; }` + // Leave them in place to avoid shifting up all the elements twice. + // We already have to do that in exit phase, so better to do it all at once. + // + // Also replace any instance property computed keys with an assignment to temp var. + // `class C { [foo()] = 123; }` -> `class C { [_foo = foo()]; }` + // Those assignments will be moved to before class in exit phase of the transform. + // -> `_foo = foo(); class C {}` + let mut instance_inits = + Vec::with_capacity(instance_prop_count + usize::from(has_instance_private_method)); + + // `_classPrivateMethodInitSpec(this, _C_brand);` + if has_instance_private_method { + instance_inits.push(self.create_class_private_method_init_spec(ctx)); + } + + let mut constructor = None; + for element in &mut body.body { + #[expect(clippy::match_same_arms)] + match element { + ClassElement::PropertyDefinition(prop) => { + // Ignore `declare` properties as they don't have any runtime effect, + // and will be removed in the TypeScript transform later + if !prop.r#static && !prop.declare { + self.convert_instance_property(prop, &mut instance_inits, ctx); + } + } + ClassElement::MethodDefinition(method) => { + if method.kind == MethodDefinitionKind::Constructor + && method.value.body.is_some() + { + constructor = Some(method.value.as_mut()); + } + } + ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => { + // TODO: Need to handle these? + } + ClassElement::StaticBlock(_) => {} + } + } + + // When `self.set_public_class_fields` and `self.remove_class_fields_without_initializer` are both true, + // we don't need to convert properties without initializers, that means `instance_prop_count != 0` but `instance_inits` may be empty. + if instance_inits.is_empty() { + return; + } + + // Scope that instance property initializers will be inserted into. + // This is usually class constructor, but can also be a `_super` function which is created. + let instance_inits_scope_id; + // Scope of class constructor, if instance property initializers will be inserted into constructor. + // Used for checking for variable name clashes. + // e.g. `class C { prop = x(); constructor(x) {} }` + // - `x` in constructor needs to be renamed when `x()` is moved into constructor body. + // `None` if class has no existing constructor, as then there can't be any clashes. + let mut instance_inits_constructor_scope_id = None; + + // Determine where to insert instance property initializers in constructor + let instance_inits_insert_location = if let Some(constructor) = constructor.as_deref_mut() { + if has_super_class { + let (insert_scopes, insert_location) = + Self::replace_super_in_constructor(constructor, ctx); + instance_inits_scope_id = insert_scopes.insert_in_scope_id; + instance_inits_constructor_scope_id = insert_scopes.constructor_scope_id; + insert_location + } else { + let constructor_scope_id = constructor.scope_id(); + instance_inits_scope_id = constructor_scope_id; + // Only record `constructor_scope_id` if constructor's scope has some bindings. + // If it doesn't, no need to check for shadowed symbols in instance prop initializers, + // because no bindings to clash with. + instance_inits_constructor_scope_id = + if ctx.scoping().get_bindings(constructor_scope_id).is_empty() { + None + } else { + Some(constructor_scope_id) + }; + InstanceInitsInsertLocation::ExistingConstructor(0) + } + } else { + // No existing constructor - create scope for one + let constructor_scope_id = ctx.scoping_mut().add_scope( + Some(class_scope_id), + NodeId::DUMMY, + ScopeFlags::Function | ScopeFlags::Constructor | ScopeFlags::StrictMode, + ); + instance_inits_scope_id = constructor_scope_id; + InstanceInitsInsertLocation::NewConstructor + }; + + // Reparent property initializers scope to `instance_inits_scope_id`. + self.reparent_initializers_scope( + instance_inits.as_slice(), + instance_inits_scope_id, + instance_inits_constructor_scope_id, + ctx, + ); + + // Insert instance initializers into constructor. + // Create a constructor if there isn't one. + match instance_inits_insert_location { + InstanceInitsInsertLocation::NewConstructor => { + Self::insert_constructor( + body, + instance_inits, + has_super_class, + instance_inits_scope_id, + ctx, + ); + } + InstanceInitsInsertLocation::ExistingConstructor(stmt_index) => { + self.insert_inits_into_constructor_as_statements( + constructor.as_mut().unwrap(), + instance_inits, + stmt_index, + ctx, + ); + } + InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding) => { + self.create_super_function_inside_constructor( + constructor.as_mut().unwrap(), + instance_inits, + &super_binding, + instance_inits_scope_id, + ctx, + ); + } + InstanceInitsInsertLocation::SuperFnOutsideClass(super_binding) => { + self.create_super_function_outside_constructor( + instance_inits, + &super_binding, + instance_inits_scope_id, + ctx, + ); + } + } + } + + /// Transform class declaration on exit. + /// + /// This is the exit phase of the transform. Only applies to class *declarations*. + /// Class *expressions* are handled in [`ClassProperties::transform_class_expression_on_exit`] below. + /// Both functions do much the same, `transform_class_expression_on_exit` inserts items as expressions, + /// whereas here we insert as statements. + /// + /// At this point, other transforms have had a chance to run on the class body, so we can move + /// parts of the code out to before/after the class now. + /// + /// * Transform static properties and insert after class. + /// * Transform static blocks and insert after class. + /// * Extract computed key assignments and insert them before class. + /// * Remove all properties and static blocks from class body. + /// + /// Items needing insertion before/after class are inserted as statements. + /// + /// `class C { static [foo()] = 123; static { bar(); } }` + /// -> `_foo = foo(); class C {}; C[_foo] = 123; bar();` + pub(super) fn transform_class_declaration_on_exit( + &mut self, + class: &mut Class<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Ignore TS class declarations + // TODO: Is this correct? + if class.declare { + return; + } + + // Leave class expressions to `transform_class_expression_on_exit` + let class_details = self.current_class(); + if !class_details.is_declaration { + return; + } + + // Finish transform + if class_details.is_transform_required { + self.transform_class_declaration_on_exit_impl(class, ctx); + } else { + debug_assert!(class_details.bindings.temp.is_none()); + } + // Pop off stack. We're done! + let class_details = self.classes_stack.pop(); + if let Some(private_props) = &class_details.private_props { + // Note: `private_props` can be non-empty even if `is_transform_required == false`, + // if class contains private accessors, which we don't transform yet + self.private_field_count -= private_props.len(); + } + } + + fn transform_class_declaration_on_exit_impl( + &mut self, + class: &mut Class<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Static properties and static blocks + self.current_class_mut().bindings.static_private_fields_use_temp = true; + + // Transform static properties, remove static and instance properties, and move computed keys + // to before class + self.transform_class_elements(class, ctx); + + // Insert temp var for class if required. Name class if required. + let class_details = self.classes_stack.last_mut(); + if let Some(temp_binding) = &class_details.bindings.temp { + // Binding for class name is required + if let Some(ident) = &class.id { + // Insert `var _Class` statement, if it wasn't already in entry phase + if !class_details.bindings.temp_var_is_created { + self.ctx.var_declarations.insert_var(temp_binding, ctx); + } + + // Insert `_Class = Class` after class. + // TODO(improve-on-babel): Could just insert `var _Class = Class;` after class, + // rather than separate `var _Class` declaration. + let class_name = + BoundIdentifier::from_binding_ident(ident).create_read_expression(ctx); + let expr = create_assignment(temp_binding, class_name, ctx); + let stmt = ctx.ast.statement_expression(SPAN, expr); + self.insert_after_stmts.insert(0, stmt); + } else { + // Class must be default export `export default class {}`, as all other class declarations + // always have a name. Set class name. + *ctx.scoping_mut().symbol_flags_mut(temp_binding.symbol_id) = SymbolFlags::Class; + class.id = Some(temp_binding.create_binding_identifier(ctx)); + } + } + + // Insert statements before/after class + let stmt_address = match ctx.parent() { + parent @ (Ancestor::ExportDefaultDeclarationDeclaration(_) + | Ancestor::ExportNamedDeclarationDeclaration(_)) => parent.address(), + // `Class` is always stored in a `Box`, so has a stable memory location + _ => Address::from_ref(class), + }; + + if !self.insert_before.is_empty() { + self.ctx.statement_injector.insert_many_before( + &stmt_address, + exprs_into_stmts(self.insert_before.drain(..), ctx), + ); + } + + if let Some(private_props) = &class_details.private_props { + if self.private_fields_as_properties { + let mut private_props = private_props + .iter() + .filter_map(|(&name, prop)| { + // TODO: Output `var _C_brand = new WeakSet();` for private instance method + if prop.is_method() || prop.is_accessor { + return None; + } + + // `var _prop = _classPrivateFieldLooseKey("prop");` + let value = Self::create_private_prop_key_loose(name, self.ctx, ctx); + Some(create_variable_declaration(&prop.binding, value, ctx)) + }) + .peekable(); + if private_props.peek().is_some() { + self.ctx.statement_injector.insert_many_before(&stmt_address, private_props); + } + } else { + let mut weakmap_symbol_id = None; + let mut has_method = false; + let mut private_props = private_props + .values() + .filter_map(|prop| { + if prop.is_static || (prop.is_method() && has_method) || prop.is_accessor { + return None; + } + if prop.is_method() { + // `var _C_brand = new WeakSet();` + has_method = true; + let binding = class_details.bindings.brand(); + let value = create_new_weakset(ctx); + Some(create_variable_declaration(binding, value, ctx)) + } else { + // `var _prop = new WeakMap();` + let value = create_new_weakmap(&mut weakmap_symbol_id, ctx); + Some(create_variable_declaration(&prop.binding, value, ctx)) + } + }) + .peekable(); + if private_props.peek().is_some() { + self.ctx.statement_injector.insert_many_before(&stmt_address, private_props); + } + } + } + + if !self.insert_after_stmts.is_empty() { + self.ctx + .statement_injector + .insert_many_after(&stmt_address, self.insert_after_stmts.drain(..)); + } + } + + /// Transform class expression on exit. + /// + /// This is the exit phase of the transform. Only applies to class *expressions*. + /// Class *expressions* are handled in [`ClassProperties::transform_class_declaration_on_exit`] above. + /// Both functions do much the same, `transform_class_declaration_on_exit` inserts items as statements, + /// whereas here we insert as expressions. + /// + /// At this point, other transforms have had a chance to run on the class body, so we can move + /// parts of the code out to before/after the class now. + /// + /// * Transform static properties and insert after class. + /// * Transform static blocks and insert after class. + /// * Extract computed key assignments and insert them before class. + /// * Remove all properties and static blocks from class body. + /// + /// Items needing insertion before/after class are inserted as expressions around, surrounding class + /// in a [`SequenceExpression`]. + /// + /// `let C = class { static [foo()] = 123; static { bar(); } };` + /// -> `let C = (_foo = foo(), _Class = class {}, _Class[_foo] = 123, bar(), _Class);` + pub(super) fn transform_class_expression_on_exit( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::ClassExpression(class) = expr else { unreachable!() }; + + // Ignore TS class declarations + // TODO: Is this correct? + if class.declare { + return; + } + + // Finish transform + let class_details = self.current_class(); + if class_details.is_transform_required { + self.transform_class_expression_on_exit_impl(expr, ctx); + } else { + debug_assert!(class_details.bindings.temp.is_none()); + } + + // Pop off stack. We're done! + let class_details = self.classes_stack.pop(); + if let Some(private_props) = &class_details.private_props { + // Note: `private_props` can be non-empty even if `is_transform_required == false`, + // if class contains private accessors, which we don't transform yet + self.private_field_count -= private_props.len(); + } + } + + fn transform_class_expression_on_exit_impl( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::ClassExpression(class) = expr else { unreachable!() }; + + // Transform static properties, remove static and instance properties, and move computed keys + // to before class + self.transform_class_elements(class, ctx); + + // Insert expressions before / after class. + // `C = class { [x()] = 1; static y = 2 };` + // -> `C = (_x = x(), _Class = class C { constructor() { this[_x] = 1; } }, _Class.y = 2, _Class)` + + // TODO: Name class if had no name, and name is statically knowable (as in example above). + // If class name shadows var which is referenced within class, rename that var. + // `var C = class { prop = C }; var C2 = C;` + // -> `var _C = class C { constructor() { this.prop = _C; } }; var C2 = _C;` + // This is really difficult as need to rename all references to that binding too, + // which can be very far above the class in AST, when it's a `var`. + // Maybe for now only add class name if it doesn't shadow a var used within class? + + // TODO: Deduct static private props and private methods from `expr_count`. + // Or maybe should store count and increment it when create private static props or private methods? + // They're probably pretty rare, so it'll be rarely used. + let class_details = self.classes_stack.last(); + + let mut expr_count = self.insert_before.len() + self.insert_after_exprs.len(); + if let Some(private_props) = &class_details.private_props { + expr_count += private_props.len(); + } + + // Exit if no expressions to insert before or after class + if expr_count == 0 { + return; + } + + expr_count += 1 + usize::from(class_details.bindings.temp.is_some()); + + let mut exprs = ctx.ast.vec_with_capacity(expr_count); + + // Insert `_prop = new WeakMap()` expressions for private instance props + // (or `_prop = _classPrivateFieldLooseKey("prop")` if loose mode). + // Babel has these always go first, regardless of order of class elements. + // Also insert `var _prop;` temp var declarations for private static props. + if let Some(private_props) = &class_details.private_props { + // Insert `var _prop;` declarations here rather than when binding was created to maintain + // same order of `var` declarations as Babel. + // `c = class C { #x = 1; static y = 2; }` -> `var _C, _x;` + // TODO(improve-on-babel): Simplify this. + if self.private_fields_as_properties { + exprs.extend(private_props.iter().filter_map(|(&name, prop)| { + // TODO: Output `_C_brand = new WeakSet()` for private instance method + if prop.is_method() || prop.is_accessor { + return None; + } + + // Insert `var _prop;` declaration + self.ctx.var_declarations.insert_var(&prop.binding, ctx); + + // `_prop = _classPrivateFieldLooseKey("prop")` + let value = Self::create_private_prop_key_loose(name, self.ctx, ctx); + Some(create_assignment(&prop.binding, value, ctx)) + })); + } else { + let mut weakmap_symbol_id = None; + let mut has_method = false; + exprs.extend(private_props.values().filter_map(|prop| { + if prop.is_accessor { + return None; + } + + if prop.is_method() { + if prop.is_static || has_method { + return None; + } + has_method = true; + // `_C_brand = new WeakSet()` + let binding = class_details.bindings.brand(); + self.ctx.var_declarations.insert_var(binding, ctx); + let value = create_new_weakset(ctx); + return Some(create_assignment(binding, value, ctx)); + } + + // Insert `var _prop;` declaration + self.ctx.var_declarations.insert_var(&prop.binding, ctx); + + if prop.is_static { + return None; + } + + // `_prop = new WeakMap()` + let value = create_new_weakmap(&mut weakmap_symbol_id, ctx); + Some(create_assignment(&prop.binding, value, ctx)) + })); + } + } + + // Insert private methods + if !self.insert_after_stmts.is_empty() { + // Find `Address` of statement containing class expression + let mut stmt_address = Address::DUMMY; + for ancestor in ctx.ancestors() { + if ancestor.is_parent_of_statement() { + break; + } + stmt_address = ancestor.address(); + } + + self.ctx + .statement_injector + .insert_many_after(&stmt_address, self.insert_after_stmts.drain(..)); + } + + // Insert computed key initializers + exprs.extend(self.insert_before.drain(..)); + + // Insert class + static property assignments + static blocks + if let Some(binding) = &class_details.bindings.temp { + // Insert `var _Class` statement, if it wasn't already in entry phase + if !class_details.bindings.temp_var_is_created { + self.ctx.var_declarations.insert_var(binding, ctx); + } + + // `_Class = class {}` + let class_expr = expr.take_in(ctx.ast); + let assignment = create_assignment(binding, class_expr, ctx); + + if exprs.is_empty() && self.insert_after_exprs.is_empty() { + // No need to wrap in sequence if no static property + // and static blocks + *expr = assignment; + return; + } + + exprs.push(assignment); + // Add static property assignments + static blocks + exprs.extend(self.insert_after_exprs.drain(..)); + // `_Class` + exprs.push(binding.create_read_expression(ctx)); + } else { + // Add static blocks (which didn't reference class name) + // TODO: If class has `extends` clause, and it may have side effects, then static block contents + // goes after class expression, and temp var is called `_temp` not `_Class`. + // `let C = class extends Unbound { static { x = 1; } };` + // -> `var _temp; let C = ((_temp = class C extends Unbound {}), (x = 1), _temp);` + // `let C = class extends Bound { static { x = 1; } };` + // -> `let C = ((x = 1), class C extends Bound {});` + exprs.extend(self.insert_after_exprs.drain(..)); + + if exprs.is_empty() { + return; + } + + let class_expr = expr.take_in(ctx.ast); + exprs.push(class_expr); + } + + debug_assert!(exprs.len() > 1); + debug_assert!(exprs.len() <= expr_count); + + *expr = ctx.ast.expression_sequence(SPAN, exprs); + } + + /// Transform class elements. + /// + /// This is part of the exit phase of transform, performed on both class declarations + /// and class expressions. + /// + /// At this point, other transforms have had a chance to run on the class body, so we can move + /// parts of the code out to before/after the class now. + /// + /// * Transform static properties and insert after class. + /// * Transform static blocks and insert after class. + /// * Transform private methods and insert after class. + /// * Extract computed key assignments and insert them before class. + /// * Remove all properties, private methods and static blocks from class body. + fn transform_class_elements(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + let mut class_methods = vec![]; + class.body.body.retain_mut(|element| { + match element { + ClassElement::PropertyDefinition(prop) => { + debug_assert!( + !prop.declare, + "`declare` property should have been removed in the TypeScript plugin's `exit_class`" + ); + if prop.r#static { + self.convert_static_property(prop, ctx); + } else if prop.computed { + self.extract_instance_prop_computed_key(prop, ctx); + } + return false; + } + ClassElement::StaticBlock(block) => { + if self.transform_static_blocks { + self.convert_static_block(block, ctx); + return false; + } + } + ClassElement::MethodDefinition(method) => { + self.substitute_temp_var_for_method_computed_key(method, ctx); + if let Some(statement) = self.convert_private_method(method, ctx) { + class_methods.push(statement); + return false; + } + } + ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => { + // TODO: Need to handle these? + } + } + + true + }); + + // All methods are moved to after the class, but need to be before static properties + // TODO(improve-on-babel): Insertion order doesn't matter, and it more clear to insert according to + // definition order. + self.insert_after_stmts.splice(0..0, class_methods); + } + + /// Flag that static private fields should be transpiled using temp binding, + /// while in this static property or static block. + /// + /// We turn `static_private_fields_use_temp` on and off when entering exiting static context. + /// + /// Static private fields reference class name (not temp var) in class declarations. + /// `class Class { static #privateProp; method() { return obj.#privateProp; } }` + /// -> `method() { return _assertClassBrand(Class, obj, _privateProp)._; }` + /// ^^^^^ + /// + /// But in static properties and static blocks, they use the temp var. + /// `class Class { static #privateProp; static publicProp = obj.#privateProp; }` + /// -> `Class.publicProp = _assertClassBrand(_Class, obj, _privateProp)._` + /// ^^^^^^ + /// + /// Also see comments on `ClassBindings`. + /// + /// Note: If declaration is `export default class {}` with no name, and class has static props, + /// then class has had name binding created already in `transform_class`. + /// So name binding is always `Some`. + pub(super) fn flag_entering_static_property_or_block(&mut self) { + // No need to check if class is a declaration, because `static_private_fields_use_temp` + // is always `true` for class expressions anyway + self.current_class_mut().bindings.static_private_fields_use_temp = true; + } + + /// Flag that static private fields should be transpiled using name binding again + /// as we're exiting this static property or static block. + /// (see [`ClassProperties::flag_entering_static_property_or_block`]) + pub(super) fn flag_exiting_static_property_or_block(&mut self) { + // Flag that transpiled static private props use name binding in class declarations + let class_details = self.current_class_mut(); + if class_details.is_declaration { + class_details.bindings.static_private_fields_use_temp = false; + } + } + + /// `_classPrivateFieldLooseKey("prop")` + fn create_private_prop_key_loose( + name: Atom<'a>, + transform_ctx: &TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + transform_ctx.helper_call_expr( + Helper::ClassPrivateFieldLooseKey, + SPAN, + ctx.ast.vec1(Argument::from(ctx.ast.expression_string_literal(SPAN, name, None))), + ctx, + ) + } + + /// Insert an expression after the class. + pub(super) fn insert_expr_after_class(&mut self, expr: Expression<'a>, ctx: &TraverseCtx<'a>) { + if self.current_class().is_declaration { + self.insert_after_stmts.push(ctx.ast.statement_expression(SPAN, expr)); + } else { + self.insert_after_exprs.push(expr); + } + } +} + +/// Create `new WeakMap()` expression. +/// +/// Takes an `&mut Option>` which is updated after looking up the binding for `WeakMap`. +/// +/// * `None` = Not looked up yet. +/// * `Some(None)` = Has been looked up, and `WeakMap` is unbound. +/// * `Some(Some(symbol_id))` = Has been looked up, and `WeakMap` has a local binding. +/// +/// This is an optimization to avoid looking up the symbol for `WeakMap` over and over when defining +/// multiple private properties. +#[expect(clippy::option_option)] +fn create_new_weakmap<'a>( + symbol_id: &mut Option>, + ctx: &mut TraverseCtx<'a>, +) -> Expression<'a> { + let symbol_id = *symbol_id + .get_or_insert_with(|| ctx.scoping().find_binding(ctx.current_scope_id(), "WeakMap")); + let ident = ctx.create_ident_expr(SPAN, Atom::from("WeakMap"), symbol_id, ReferenceFlags::Read); + ctx.ast.expression_new_with_pure(SPAN, ident, NONE, ctx.ast.vec(), true) +} + +/// Create `new WeakSet()` expression. +fn create_new_weakset<'a>(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + let symbol_id = ctx.scoping().find_binding(ctx.current_scope_id(), "WeakSet"); + let ident = ctx.create_ident_expr(SPAN, Atom::from("WeakSet"), symbol_id, ReferenceFlags::Read); + ctx.ast.expression_new_with_pure(SPAN, ident, NONE, ctx.ast.vec(), true) +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/class_bindings.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/class_bindings.rs new file mode 100644 index 000000000000..6202e37e7e48 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/class_bindings.rs @@ -0,0 +1,145 @@ +use oxc_syntax::{ + scope::ScopeId, + symbol::{SymbolFlags, SymbolId}, +}; +use oxc_traverse::BoundIdentifier; + +use crate::context::TraverseCtx; + +/// Store for bindings for class. +/// +/// 1. Existing binding for class name (if class has a name). +/// 2. Temp var `_Class`, which may or may not be required. +/// +/// Temp var is required in the following circumstances: +/// +/// * Class expression has static properties. +/// e.g. `C = class { static x = 1; }` +/// * Class declaration has static properties and one of the static prop's initializers contains: +/// a. `this` +/// e.g. `class C { static x = this; }` +/// b. Reference to class name +/// e.g. `class C { static x = C; }` +/// c. A private field referring to one of the class's static private props. +/// e.g. `class C { static #x; static y = obj.#x; }` +/// +/// The logic for when transpiled private fields use a reference to class name or class temp var +/// is unfortunately rather complicated. +/// +/// Transpiled private fields referring to a static private prop use: +/// +/// * Class name when field is within body of class declaration +/// e.g. `class C { static #x; method() { return obj.#x; } }` +/// -> `_assertClassBrand(C, obj, _x)._` +/// * Temp var when field is within body of class expression +/// e.g. `C = class C { static #x; method() { return obj.#x; } }` +/// -> `_assertClassBrand(_C, obj, _x)._` +/// * Temp var when field is within a static prop initializer +/// e.g. `class C { static #x; static y = obj.#x; }` +/// -> `_assertClassBrand(_C, obj, _x)._` +/// +/// `static_private_fields_use_temp` is updated as transform moves through the class, +/// to indicate which binding to use. +pub(super) struct ClassBindings<'a> { + /// Binding for class name, if class has name + pub name: Option>, + /// Temp var for class. + /// e.g. `_Class` in `_Class = class {}, _Class.x = 1, _Class` + pub temp: Option>, + /// Temp var for WeakSet. + pub brand: Option>, + /// `ScopeId` of hoist scope outside class (which temp `var` binding would be created in) + pub outer_hoist_scope_id: ScopeId, + /// `true` if should use temp binding for references to class in transpiled static private fields, + /// `false` if can use name binding + pub static_private_fields_use_temp: bool, + /// `true` if temp var for class has been inserted + pub temp_var_is_created: bool, +} + +impl<'a> ClassBindings<'a> { + /// Create new `ClassBindings`. + pub fn new( + name_binding: Option>, + temp_binding: Option>, + brand_binding: Option>, + outer_scope_id: ScopeId, + static_private_fields_use_temp: bool, + temp_var_is_created: bool, + ) -> Self { + Self { + name: name_binding, + temp: temp_binding, + brand: brand_binding, + outer_hoist_scope_id: outer_scope_id, + static_private_fields_use_temp, + temp_var_is_created, + } + } + + /// Create dummy `ClassBindings`. + /// + /// Used when class needs no transform, and for dummy entry at top of `ClassesStack`. + pub fn dummy() -> Self { + Self::new(None, None, None, ScopeId::new(0), false, false) + } + + /// Get `SymbolId` of name binding. + pub fn name_symbol_id(&self) -> Option { + self.name.as_ref().map(|binding| binding.symbol_id) + } + + /// Get [`BoundIdentifier`] for class brand. + /// + /// Only use this method when you are sure that [Self::brand] is not `None`, + /// this will happen when there is a private method in the class. + /// + /// # Panics + /// Panics if [Self::brand] is `None`. + pub fn brand(&self) -> &BoundIdentifier<'a> { + self.brand.as_ref().unwrap() + } + + /// Get binding to use for referring to class in transpiled static private fields. + /// + /// e.g. `Class` in `_assertClassBrand(Class, object, _prop)._` (class name) + /// or `_Class` in `_assertClassBrand(_Class, object, _prop)._` (temp var) + /// + /// * In class expressions, this is always be temp binding. + /// * In class declarations, it's the name binding when code is inside class body, + /// and temp binding when code is outside class body. + /// + /// `static_private_fields_use_temp` is set accordingly at the right moments + /// elsewhere in this transform. + /// + /// If a temp binding is required, and one doesn't already exist, a temp binding is created. + pub fn get_or_init_static_binding( + &mut self, + ctx: &mut TraverseCtx<'a>, + ) -> &BoundIdentifier<'a> { + if self.static_private_fields_use_temp { + // Create temp binding if doesn't already exist + self.temp.get_or_insert_with(|| { + Self::create_temp_binding(self.name.as_ref(), self.outer_hoist_scope_id, ctx) + }) + } else { + // `static_private_fields_use_temp` is always `true` for class expressions. + // Class declarations always have a name binding if they have any static props. + // So `unwrap` here cannot panic. + self.name.as_ref().unwrap() + } + } + + /// Generate binding for temp var. + pub fn create_temp_binding( + name_binding: Option<&BoundIdentifier<'a>>, + outer_hoist_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + // Base temp binding name on class name, or "Class" if no name. + // TODO(improve-on-babel): If class name var isn't mutated, no need for temp var for + // class declaration. Can just use class binding. + let name = name_binding.map_or("Class", |binding| binding.name.as_str()); + ctx.generate_uid(name, outer_hoist_scope_id, SymbolFlags::FunctionScopedVariable) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/class_details.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/class_details.rs new file mode 100644 index 000000000000..b43c294a4ae0 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/class_details.rs @@ -0,0 +1,282 @@ +use oxc_ast::ast::*; +use oxc_data_structures::stack::NonEmptyStack; +use oxc_span::Atom; +use oxc_traverse::BoundIdentifier; + +use super::{ClassBindings, ClassProperties, FxIndexMap}; + +/// Details of a class. +/// +/// These are stored in `ClassesStack`. +pub(super) struct ClassDetails<'a> { + /// `true` for class declaration, `false` for class expression + pub is_declaration: bool, + /// `true` if class requires no transformation + pub is_transform_required: bool, + /// Private properties. + /// Mapping private prop name to binding for temp var. + /// This is then used as lookup when transforming e.g. `this.#x`. + /// `None` if class has no private properties. + pub private_props: Option, PrivateProp<'a>>>, + /// Bindings for class name and temp var for class + pub bindings: ClassBindings<'a>, +} + +impl ClassDetails<'_> { + /// Create dummy `ClassDetails`. + /// + /// Used for dummy entry at top of `ClassesStack`. + pub fn dummy(is_declaration: bool) -> Self { + Self { + is_declaration, + is_transform_required: false, + private_props: None, + bindings: ClassBindings::dummy(), + } + } +} + +/// Details of a private property. +pub(super) struct PrivateProp<'a> { + pub binding: BoundIdentifier<'a>, + pub is_static: bool, + pub method_kind: Option, + pub is_accessor: bool, + // For accessor methods, they have two bindings, + // one for getter and another for setter. + pub binding2: Option>, +} + +impl<'a> PrivateProp<'a> { + pub fn new( + binding: BoundIdentifier<'a>, + is_static: bool, + method_kind: Option, + is_accessor: bool, + ) -> Self { + Self { binding, is_static, method_kind, is_accessor, binding2: None } + } + + pub fn is_method(&self) -> bool { + self.method_kind.is_some() + } + + pub fn is_accessor(&self) -> bool { + self.is_accessor || self.method_kind.is_some_and(MethodDefinitionKind::is_accessor) + } + + pub fn set_binding2(&mut self, binding: BoundIdentifier<'a>) { + self.binding2 = Some(binding); + } +} + +/// Stack of `ClassDetails`. +/// +/// Pushed to when entering a class, popped when exiting. +/// +/// We use a `NonEmptyStack` to make `last` and `last_mut` cheap (these are used a lot). +/// The first entry is a dummy. +/// +/// This is a separate structure, rather than just storing stack as a property of `ClassProperties` +/// to work around borrow-checker. You can call `find_private_prop` and retain the return value +/// without holding a mut borrow of the whole of `&mut ClassProperties`. This allows accessing other +/// properties of `ClassProperties` while that borrow is held. +pub(super) struct ClassesStack<'a> { + stack: NonEmptyStack>, +} + +impl<'a> ClassesStack<'a> { + /// Create new `ClassesStack`. + pub fn new() -> Self { + // Default stack capacity is 4. That's is probably good. More than 4 nested classes is rare. + Self { stack: NonEmptyStack::new(ClassDetails::dummy(false)) } + } + + /// Push an entry to stack. + #[inline] + pub fn push(&mut self, class: ClassDetails<'a>) { + self.stack.push(class); + } + + /// Push last entry from stack. + #[inline] + pub fn pop(&mut self) -> ClassDetails<'a> { + self.stack.pop() + } + + /// Get details of current class. + #[inline] + pub fn last(&self) -> &ClassDetails<'a> { + self.stack.last() + } + + /// Get details of current class as `&mut` reference. + #[inline] + pub fn last_mut(&mut self) -> &mut ClassDetails<'a> { + self.stack.last_mut() + } + + fn lookup_private_prop< + 'b, + Ret, + RetFn: Fn(&'b PrivateProp<'a>, &'b mut ClassBindings<'a>, bool) -> Ret, + >( + &'b mut self, + ident: &PrivateIdentifier<'a>, + ret_fn: RetFn, + ) -> Ret { + // Check for binding in closest class first, then enclosing classes. + // We skip the first, because this is a `NonEmptyStack` with dummy first entry. + // TODO: Check there are tests for bindings in enclosing classes. + for class in self.stack[1..].iter_mut().rev() { + if let Some(private_props) = &mut class.private_props + && let Some(prop) = private_props.get(&ident.name) + { + return ret_fn(prop, &mut class.bindings, class.is_declaration); + } + } + unreachable!(); + } + + /// Lookup details of private property referred to by `ident`. + pub fn find_private_prop<'b>( + &'b mut self, + ident: &PrivateIdentifier<'a>, + ) -> ResolvedPrivateProp<'a, 'b> { + self.lookup_private_prop(ident, move |prop, class_bindings, is_declaration| { + ResolvedPrivateProp { + prop_binding: &prop.binding, + class_bindings, + is_static: prop.is_static, + is_method: prop.is_method(), + is_accessor: prop.is_accessor(), + is_declaration, + } + }) + } + + /// Lookup details of readable private property referred to by `ident`. + pub fn find_readable_private_prop<'b>( + &'b mut self, + ident: &PrivateIdentifier<'a>, + ) -> Option> { + self.lookup_private_prop(ident, move |prop, class_bindings, is_declaration| { + let prop_binding = if matches!(prop.method_kind, Some(MethodDefinitionKind::Set)) { + prop.binding2.as_ref() + } else { + Some(&prop.binding) + }; + prop_binding.map(|prop_binding| ResolvedPrivateProp { + prop_binding, + class_bindings, + is_static: prop.is_static, + is_method: prop.is_method(), + is_accessor: prop.is_accessor(), + is_declaration, + }) + }) + } + + /// Lookup details of writeable private property referred to by `ident`. + /// Returns `Some` if it refers to a private prop and setter method + pub fn find_writeable_private_prop<'b>( + &'b mut self, + ident: &PrivateIdentifier<'a>, + ) -> Option> { + self.lookup_private_prop(ident, move |prop, class_bindings, is_declaration| { + let prop_binding = if matches!(prop.method_kind, Some(MethodDefinitionKind::Set) | None) + { + Some(&prop.binding) + } else { + prop.binding2.as_ref() + }; + prop_binding.map(|prop_binding| ResolvedPrivateProp { + prop_binding, + class_bindings, + is_static: prop.is_static, + is_method: prop.is_method(), + is_accessor: prop.is_accessor(), + is_declaration, + }) + }) + } + + /// Look up details of the private property referred to by ident and it can either be read or written. + pub fn find_get_set_private_prop<'b>( + &'b mut self, + ident: &PrivateIdentifier<'a>, + ) -> ResolvedGetSetPrivateProp<'a, 'b> { + self.lookup_private_prop(ident, move |prop, class_bindings, is_declaration| { + let (get_binding, set_binding) = match prop.method_kind { + Some(MethodDefinitionKind::Set) => (prop.binding2.as_ref(), Some(&prop.binding)), + Some(_) => (Some(&prop.binding), prop.binding2.as_ref()), + _ => (Some(&prop.binding), Some(&prop.binding)), + }; + ResolvedGetSetPrivateProp { + get_binding, + set_binding, + class_bindings, + is_static: prop.is_static, + is_method: prop.is_method(), + is_accessor: prop.is_accessor(), + is_declaration, + } + }) + } +} + +/// Details of a private property resolved for a private field. +/// +/// This is the return value of [`ClassesStack::find_private_prop`], +/// [`ClassesStack::find_readable_private_prop`] and +/// [`ClassesStack::find_writeable_private_prop`]. +pub(super) struct ResolvedPrivateProp<'a, 'b> { + /// Binding for temp var representing the property + pub prop_binding: &'b BoundIdentifier<'a>, + /// Bindings for class name and temp var for class + pub class_bindings: &'b mut ClassBindings<'a>, + /// `true` if is a static property + pub is_static: bool, + /// `true` if is a private method or accessor property + pub is_method: bool, + /// `true` if is a private accessor property or [`PrivateProp::method_kind`] is + /// `Some(MethodDefinitionKind::Get)` or `Some(MethodDefinitionKind::Set)` + pub is_accessor: bool, + /// `true` if class which defines this property is a class declaration + pub is_declaration: bool, +} + +/// Details of a private property resolved for a private field. +/// +/// This is the return value of [`ClassesStack::find_get_set_private_prop`]. +pub(super) struct ResolvedGetSetPrivateProp<'a, 'b> { + /// Binding for temp var representing the property or getter method + pub get_binding: Option<&'b BoundIdentifier<'a>>, + /// Binding for temp var representing the property or setter method + pub set_binding: Option<&'b BoundIdentifier<'a>>, + /// Bindings for class name and temp var for class + pub class_bindings: &'b mut ClassBindings<'a>, + /// `true` if is a static property + pub is_static: bool, + /// `true` if is a private method or accessor property + pub is_method: bool, + /// `true` if is a private accessor property or [`PrivateProp::method_kind`] is + /// `Some(MethodDefinitionKind::Get)` or `Some(MethodDefinitionKind::Set)` + #[expect(unused)] + pub is_accessor: bool, + /// `true` if class which defines this property is a class declaration + pub is_declaration: bool, +} + +// Shortcut methods to get current class +impl<'a> ClassProperties<'a, '_> { + /// Get details of current class. + pub(super) fn current_class(&self) -> &ClassDetails<'a> { + self.classes_stack.last() + } + + /// Get details of current class as `&mut` reference. + pub(super) fn current_class_mut(&mut self) -> &mut ClassDetails<'a> { + self.classes_stack.last_mut() + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/computed_key.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/computed_key.rs new file mode 100644 index 000000000000..6179ca326915 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/computed_key.rs @@ -0,0 +1,163 @@ +//! ES2022: Class Properties +//! Transform of class property/method computed keys. + +use oxc_allocator::TakeIn; +use oxc_ast::ast::*; + +use crate::context::TraverseCtx; + +use super::ClassProperties; + +impl<'a> ClassProperties<'a, '_> { + /// Substitute temp var for method computed key. + /// `class C { [x()]() {} }` -> `let _x; _x = x(); class C { [_x]() {} }` + /// This transform is only required if class has properties or a static block. + pub(super) fn substitute_temp_var_for_method_computed_key( + &mut self, + method: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Exit if key is not an `Expression` + // (`PropertyKey::StaticIdentifier` or `PropertyKey::PrivateIdentifier`) + let Some(key) = method.key.as_expression_mut() else { + return; + }; + + // Exit if evaluating key cannot have side effects. + // This check also results in exit for non-computed keys e.g. `class C { 'x'() {} 123() {} }`. + if !self.ctx.key_needs_temp_var(key, ctx) { + return; + } + + // TODO(improve-on-babel): It's unnecessary to create temp vars for method keys unless: + // 1. Properties also have computed keys. + // 2. Some of those properties' computed keys have side effects and require temp vars. + // 3. At least one property satisfying the above is after this method, + // or class contains a static block which is being transformed + // (static blocks are always evaluated after computed keys, regardless of order) + let original_key = key.take_in(ctx.ast); + let (assignment, temp_var) = self.ctx.create_computed_key_temp_var(original_key, ctx); + self.insert_before.push(assignment); + method.key = PropertyKey::from(temp_var); + } + + /// Convert computed property/method key to a temp var, if a temp var is required. + /// + /// If no temp var is required, take ownership of key, and return it. + /// + /// Transformation is: + /// * Class declaration: + /// `class C { [x()] = 1; }` -> `let _x; _x = x(); class C { constructor() { this[_x] = 1; } }` + /// * Class expression: + /// `C = class { [x()] = 1; }` -> `let _x; C = (_x = x(), class C { constructor() { this[_x] = 1; } })` + /// + /// This function: + /// * Creates the `let _x;` statement and inserts it. + /// * Creates the `_x = x()` assignment. + /// * If static prop, inserts assignment before class. + /// * If instance prop, replaces existing key with assignment (it'll be moved to before class later). + /// * Returns `_x`. + pub(super) fn create_computed_key_temp_var_if_required( + &mut self, + key: &mut Expression<'a>, + is_static: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let original_key = key.take_in(ctx.ast); + if self.ctx.key_needs_temp_var(&original_key, ctx) { + let (assignment, ident) = self.ctx.create_computed_key_temp_var(original_key, ctx); + if is_static { + self.insert_before.push(assignment); + } else { + *key = assignment; + } + ident + } else { + original_key + } + } + + /// Extract computed key when key needs a temp var, which may have side effect. + /// + /// When `set_public_class_fields` and `remove_class_fields_without_initializer` are both true, + /// fields without initializers would be removed. However, if the key is a computed key and may + /// have side effects, we need to extract the key and place it before the class to preserve the + /// original behavior. + /// + /// Extract computed key: + /// `class C { [foo()] }` + /// -> `foo(); class C { }` + /// + /// Do not extract computed key: + /// `class C { [123] }` + /// -> `class C { }` + /// + pub(super) fn extract_computed_key( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &TraverseCtx<'a>, + ) { + let Some(key) = prop.key.as_expression_mut() else { + return; + }; + + if self.ctx.key_needs_temp_var(key, ctx) { + self.insert_before.push(key.take_in(ctx.ast)); + } + } + + /// Extract computed key if it's an assignment, and replace with identifier. + /// + /// In entry phase, computed keys for instance properties are converted to assignments to temp vars. + /// `class C { [foo()] = 123 }` + /// -> `class C { [_foo = foo()]; constructor() { this[_foo] = 123; } }` + /// + /// Now in exit phase, extract this assignment and move it to before class. + /// + /// `class C { [_foo = foo()]; constructor() { this[_foo] = 123; } }` + /// -> `_foo = foo(); class C { [null]; constructor() { this[_foo] = 123; } }` + /// (`[null]` property will be removed too by caller) + /// + /// We do this process in 2 passes so that the computed key is still present within the class during + /// traversal of the class body, so any other transforms can run on it. + /// Now that we're exiting the class, we can move the assignment `_foo = foo()` out of the class + /// to where it needs to be. + pub(super) fn extract_instance_prop_computed_key( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &TraverseCtx<'a>, + ) { + // Exit if computed key is not an assignment (wasn't processed in 1st pass) + if !matches!(&prop.key, PropertyKey::AssignmentExpression(_)) { + // This field is going to be removed, but if the key is a computed key and may have + // side effects, we need to extract the key and place it before the class to preserve + // the original behavior. + if prop.value.is_none() + && self.set_public_class_fields + && self.remove_class_fields_without_initializer + { + self.extract_computed_key(prop, ctx); + } + + return; + } + + // Debug checks that we're removing what we think we are + #[cfg(debug_assertions)] + { + let PropertyKey::AssignmentExpression(assign_expr) = &prop.key else { unreachable!() }; + assert!(assign_expr.span.is_empty()); + let AssignmentTarget::AssignmentTargetIdentifier(ident) = &assign_expr.left else { + unreachable!(); + }; + assert!(ident.name.starts_with('_')); + assert!(ctx.scoping().get_reference(ident.reference_id()).symbol_id().is_some()); + assert!(ident.span.is_empty()); + assert!(prop.value.is_none()); + } + + // Extract assignment from computed key and insert before class + let assignment = prop.key.take_in(ctx.ast).into_expression(); + self.insert_before.push(assignment); + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/constructor.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/constructor.rs new file mode 100644 index 000000000000..0f9eb37b0c2b --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/constructor.rs @@ -0,0 +1,796 @@ +//! ES2022: Class Properties +//! Insertion of instance property initializers into constructor. +//! +//! When a class has instance properties / instance private properties, we need to either: +//! 1. Move initialization of these properties into existing constructor, or +//! 2. Add a constructor to the class containing property initializers. +//! +//! Oxc's output uses Babel's helpers (`_defineProperty`, `_classPrivateFieldInitSpec` etc). +//! +//! ## Output vs Babel and ESBuild +//! +//! Oxc's output follows Babel where: +//! 1. the class has no super class, or +//! 2. the class has no constructor, or +//! 3. constructor only contains a single `super()` call at top level of the function. +//! +//! Where a class with superclass has an existing constructor containing 1 or more `super()` calls +//! nested within the constructor, we do more like ESBuild does. We insert a single arrow function +//! `_super` at top of the function and replace all `super()` calls with `_super()`. +//! +//! Input: +//! ```js +//! class C extends S { +//! prop = 1; +//! constructor(yes) { +//! if (yes) { +//! super(2); +//! } else { +//! super(3); +//! } +//! } +//! } +//! ``` +//! +//! Babel output: +//! ```js +//! class C extends S { +//! constructor(yes) { +//! if (yes) { +//! super(2); +//! this.prop = foo(); +//! } else { +//! super(3); +//! this.prop = foo(); +//! } +//! } +//! } +//! ``` +//! [Babel REPL](https://babeljs.io/repl#?code_lz=MYGwhgzhAEDC0FMAeAXBA7AJjAytA3gFDTQAOATgPanQC80AZpZQBQCUA3MdMJehCnIBXYCkrkWATwQQ2BbiQCWDaFJlyiJLdAhDSCCQCZOC6AF9EICAnnaSu_RIDMJ7We7uzQA&presets=&externalPlugins=%40babel%2Fplugin-transform-class-properties%407.25.9&assumptions=%7B%22setPublicClassFields%22%3Atrue%7D) +//! +//! Oxc output: +//! ```js +//! class C extends S { +//! constructor(yes) { +//! var _super = (..._args) => ( +//! super(..._args), +//! this.prop = foo(), +//! this +//! ); +//! if (yes) { +//! _super(2); +//! } else { +//! _super(3); +//! } +//! } +//! } +//! ``` +//! ESBuild's output: [ESBuild REPL](https://esbuild.github.io/try/#dAAwLjI0LjAALS10YXJnZXQ9ZXMyMDIwAGNsYXNzIEMgZXh0ZW5kcyBTIHsKICBwcm9wID0gZm9vKCk7CiAgY29uc3RydWN0b3IoeWVzKSB7CiAgICBpZiAoeWVzKSB7CiAgICAgIHN1cGVyKDIpOwogICAgfSBlbHNlIHsKICAgICAgc3VwZXIoMyk7CiAgICB9CiAgfQp9) +//! +//! ## `super()` in constructor params +//! +//! Babel handles this case correctly for standard properties, but Babel's approach is problematic for us +//! because Babel outputs the property initializers twice if there are 2 x `super()` calls. +//! We would need to use `CloneIn` and then duplicate all the `ReferenceId`s etc. +//! +//! Instead, we create a `_super` function containing property initializers *outside* the class +//! and convert `super()` calls to `_super(super())`. +//! +//! Input: +//! ```js +//! class C extends S { +//! prop = foo(); +//! constructor(x = super(), y = super()) {} +//! } +//! ``` +//! +//! Oxc output: +//! ```js +//! let _super = function() { +//! "use strict"; +//! this.prop = foo(); +//! return this; +//! }; +//! class C extends S { +//! constructor(x = _super.call(super()), y = _super.call(super())) {} +//! } +//! ``` +//! +//! ESBuild does not handle `super()` in constructor params correctly: +//! [ESBuild REPL](https://esbuild.github.io/try/#dAAwLjI0LjAALS10YXJnZXQ9ZXMyMDIwAGNsYXNzIEMgZXh0ZW5kcyBTIHsKICBwcm9wID0gZm9vKCk7CiAgY29uc3RydWN0b3IoeCA9IHN1cGVyKCksIHkgPSBzdXBlcigpKSB7fQp9Cg) + +use std::iter; + +use oxc_allocator::TakeIn; +use rustc_hash::FxHashMap; + +use oxc_ast::{NONE, ast::*}; +use oxc_ast_visit::{VisitMut, walk_mut}; +use oxc_span::SPAN; +use oxc_syntax::{ + node::NodeId, + scope::{ScopeFlags, ScopeId}, + symbol::{SymbolFlags, SymbolId}, +}; +use oxc_traverse::BoundIdentifier; + +use crate::{ + context::TraverseCtx, + utils::ast_builder::{ + create_assignment, create_class_constructor_with_params, create_super_call, + }, +}; + +use super::{ClassProperties, utils::exprs_into_stmts}; + +/// Location to insert instance property initializers +pub(super) enum InstanceInitsInsertLocation<'a> { + /// Create new constructor, containing initializers + NewConstructor, + /// Insert initializers into existing constructor at this statement index + ExistingConstructor(usize), + /// Create a `_super` function inside class constructor, containing initializers + SuperFnInsideConstructor(BoundIdentifier<'a>), + /// Create a `_super` function outside class, containing initializers + SuperFnOutsideClass(BoundIdentifier<'a>), +} + +/// Scopes related to inserting and transforming instance property initializers +pub(super) struct InstanceInitScopes { + /// Scope that instance prop initializers will be inserted into + pub insert_in_scope_id: ScopeId, + /// Scope of class constructor, if initializers will be inserted into constructor, + /// (either directly, or in `_super` function within constructor) + /// and constructor's scope contains any bindings. + /// This is used for renaming symbols if any shadow symbols referenced by instance prop initializers. + pub constructor_scope_id: Option, +} + +impl<'a> ClassProperties<'a, '_> { + /// Replace `super()` call(s) in constructor, if required. + /// + /// Returns: + /// * `InstanceInitScopes` detailing the `ScopeId`s required for transforming instance property initializers. + /// * `InstanceInitsInsertLocation` detailing where instance property initializers should be inserted. + /// + /// * `super()` first appears as a top level statement in constructor body (common case): + /// * Do not alter constructor. + /// * No `_super` function is required. + /// * Returns `InstanceInitsInsertLocation::Statements`, specifying statement index + /// where inits should be inserted. + /// * `super()` is used in function params: + /// * Replace `super()` calls with `_super.call(super())`. + /// * `_super` function will need to be inserted outside class. + /// * Returns `InstanceInitsInsertLocation::SuperFnOutsideClass`. + /// * `super()` is found elsewhere in constructor: + /// * Replace `super()` calls with `_super()`. + /// * `_super` function will need to be inserted at top of class constructor. + /// * Returns `InstanceInitsInsertLocation::SuperFnInsideConstructor`. + /// * `super()` in constructor params or body: + /// * `_super` function will need to be inserted at top of class constructor. + /// * Returns `InstanceInitsInsertLocation::SuperFnInsideConstructor`. + /// + /// See doc comment at top of this file for more details of last 3 cases. + /// + /// If a `_super` function is required, binding for `_super` is recorded in the returned + /// `InstanceInitsInsertLocation`, and `ScopeId` for `_super` function is returned as + /// `insert_in_scope_id` in returned `InstanceInitScopes`. + /// + /// This function does not create the `_super` function or insert it. That happens later. + pub(super) fn replace_super_in_constructor( + constructor: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (InstanceInitScopes, InstanceInitsInsertLocation<'a>) { + // Find any `super()`s in constructor params and replace with `_super.call(super())` + let replacer = ConstructorParamsSuperReplacer::new(ctx); + if let Some((super_func_scope_id, insert_location)) = replacer.replace(constructor) { + // `super()` found in constructor's params. + // Property initializers will be inserted in a `_super` function *outside* class. + let insert_scopes = InstanceInitScopes { + insert_in_scope_id: super_func_scope_id, + constructor_scope_id: None, + }; + return (insert_scopes, insert_location); + } + + // No `super()` in constructor params. + // Property initializers will be inserted after `super()` statement, + // or in a `_super` function inserted at top of constructor. + let constructor_scope_id = constructor.scope_id(); + let replacer = ConstructorBodySuperReplacer::new(constructor_scope_id, ctx); + let (super_func_scope_id, insert_location) = replacer.replace(constructor); + + // Only include `constructor_scope_id` in return value if constructor's scope has some bindings. + // If it doesn't, no need to check for shadowed symbols in instance prop initializers, + // because no bindings to clash with. + let constructor_scope_id = if ctx.scoping().get_bindings(constructor_scope_id).is_empty() { + None + } else { + Some(constructor_scope_id) + }; + + let insert_scopes = + InstanceInitScopes { insert_in_scope_id: super_func_scope_id, constructor_scope_id }; + + (insert_scopes, insert_location) + } + + // TODO: Handle private props in constructor params `class C { #x; constructor(x = this.#x) {} }`. + + /// Add a constructor to class containing property initializers. + pub(super) fn insert_constructor( + body: &mut ClassBody<'a>, + inits: Vec>, + has_super_class: bool, + constructor_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) { + // Create statements to go in function body + let mut stmts = ctx.ast.vec_with_capacity(inits.len() + usize::from(has_super_class)); + + // Add `super(..._args);` statement and `..._args` param if class has a super class. + // `constructor(..._args) { super(..._args); /* prop initialization */ }` + let mut params_rest = None; + if has_super_class { + let args_binding = + ctx.generate_uid("args", constructor_scope_id, SymbolFlags::FunctionScopedVariable); + params_rest = Some( + ctx.ast.alloc_binding_rest_element(SPAN, args_binding.create_binding_pattern(ctx)), + ); + stmts.push(ctx.ast.statement_expression(SPAN, create_super_call(&args_binding, ctx))); + } + // TODO: Should these have the span of the original `PropertyDefinition`s? + stmts.extend(exprs_into_stmts(inits, ctx)); + + let params = ctx.ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + ctx.ast.vec(), + params_rest, + ); + + let ctor = create_class_constructor_with_params(stmts, params, constructor_scope_id, ctx); + + // TODO(improve-on-babel): Could push constructor onto end of elements, instead of inserting as first + body.body.insert(0, ctor); + } + + /// Insert instance property initializers into constructor body at `insertion_index`. + pub(super) fn insert_inits_into_constructor_as_statements( + &mut self, + constructor: &mut Function<'a>, + inits: Vec>, + insertion_index: usize, + ctx: &mut TraverseCtx<'a>, + ) { + // Rename any symbols in constructor which clash with references in inits + self.rename_clashing_symbols(constructor, ctx); + + // Insert inits into constructor body + let body_stmts = &mut constructor.body.as_mut().unwrap().statements; + body_stmts.splice(insertion_index..insertion_index, exprs_into_stmts(inits, ctx)); + } + + /// Create `_super` function containing instance property initializers, + /// and insert at top of constructor body. + /// `var _super = (..._args) => (super(..._args), , this);` + pub(super) fn create_super_function_inside_constructor( + &mut self, + constructor: &mut Function<'a>, + inits: Vec>, + super_binding: &BoundIdentifier<'a>, + super_func_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) { + // Rename any symbols in constructor which clash with references in inits + self.rename_clashing_symbols(constructor, ctx); + + // `(super(..._args), , this)` + // + // TODO(improve-on-babel): When not in loose mode, inits are `_defineProperty(this, propName, value)`. + // `_defineProperty` returns `this`, so last statement could be `return _defineProperty(this, propName, value)`, + // rather than an additional `return this` statement. + // Actually this wouldn't work at present, as `_classPrivateFieldInitSpec(this, _prop, value)` + // does not return `this`. We could alter it so it does when we have our own helper package. + let args_binding = + ctx.generate_uid("args", super_func_scope_id, SymbolFlags::FunctionScopedVariable); + let super_call = create_super_call(&args_binding, ctx); + let this_expr = ctx.ast.expression_this(SPAN); + let body_exprs = ctx.ast.expression_sequence( + SPAN, + ctx.ast.vec_from_iter(iter::once(super_call).chain(inits).chain(iter::once(this_expr))), + ); + let body = ctx.ast.vec1(ctx.ast.statement_expression(SPAN, body_exprs)); + + // `(..._args) => (super(..._args), , this)` + let super_func = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife( + SPAN, + true, + false, + NONE, + ctx.ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::ArrowFormalParameters, + ctx.ast.vec(), + Some( + ctx.ast + .alloc_binding_rest_element(SPAN, args_binding.create_binding_pattern(ctx)), + ), + ), + NONE, + ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), body), + super_func_scope_id, + false, + false, + ); + + // `var _super = (..._args) => ( ... );` + let super_func_decl = Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + super_binding.create_binding_pattern(ctx), + Some(super_func), + false, + )), + false, + )); + + // Insert at top of function + let body_stmts = &mut constructor.body.as_mut().unwrap().statements; + body_stmts.insert(0, super_func_decl); + } + + /// Create `_super` function containing instance property initializers, + /// and insert it outside class. + /// `let _super = function() { ; return this; }` + pub(super) fn create_super_function_outside_constructor( + &mut self, + inits: Vec>, + super_binding: &BoundIdentifier<'a>, + super_func_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) { + // Add `"use strict"` directive if outer scope is not strict mode + // TODO: This should be parent scope if insert `_super` function as expression before class expression. + let outer_scope_id = ctx.current_block_scope_id(); + let directives = if ctx.scoping().scope_flags(outer_scope_id).is_strict_mode() { + ctx.ast.vec() + } else { + ctx.ast.vec1(ctx.ast.use_strict_directive()) + }; + + // `return this;` + let return_stmt = ctx.ast.statement_return(SPAN, Some(ctx.ast.expression_this(SPAN))); + // `; return this;` + let body_stmts = ctx.ast.vec_from_iter(exprs_into_stmts(inits, ctx).chain([return_stmt])); + // `function() { ; return this; }` + let super_func = ctx.ast.expression_function_with_scope_id_and_pure_and_pife( + SPAN, + FunctionType::FunctionExpression, + 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, directives, body_stmts)), + super_func_scope_id, + false, + false, + ); + + // Insert `_super` function after class. + // TODO: Need to add `_super` function to class as a static method, and then remove it again + // in exit phase - so other transforms run on it in between. + // TODO: Need to transform `super` and references to class name in initializers. + // TODO: If static block transform is not enabled, it's possible to construct the class + // within the static block `class C { static { new C() } }` and that'd run before `_super` + // is defined. So it needs to go before the class, not after, in that case. + let init = if self.current_class().is_declaration { + Some(super_func) + } else { + let assignment = create_assignment(super_binding, super_func, ctx); + // TODO: Why does this end up before class, not after? + // TODO: This isn't right. Should not be adding to `insert_after_exprs` in entry phase. + self.insert_after_exprs.push(assignment); + None + }; + self.ctx.var_declarations.insert_let(super_binding, init, ctx); + } + + /// Rename any symbols in constructor which clash with symbols used in initializers + fn rename_clashing_symbols( + &mut self, + constructor: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let clashing_symbols = &mut self.clashing_constructor_symbols; + if clashing_symbols.is_empty() { + return; + } + + // Rename symbols to UIDs + let constructor_scope_id = constructor.scope_id(); + for (&symbol_id, name) in clashing_symbols.iter_mut() { + // Generate replacement UID name + let new_name = ctx.generate_uid_name(name); + // Save replacement name in `clashing_symbols` + *name = new_name; + // Rename symbol and binding + ctx.scoping_mut().rename_symbol(symbol_id, constructor_scope_id, new_name.as_str()); + } + + // Rename identifiers for clashing symbols in constructor params and body + let mut renamer = ConstructorSymbolRenamer::new(clashing_symbols, ctx); + renamer.visit_function(constructor, ScopeFlags::empty()); + + // Empty `clashing_constructor_symbols` hashmap for reuse on next class + clashing_symbols.clear(); + } +} + +/// Visitor for transforming `super()` in class constructor params. +struct ConstructorParamsSuperReplacer<'a, 'ctx> { + /// Binding for `_super` function. + /// Initially `None`. Binding is created if `super()` is found. + super_binding: Option>, + ctx: &'ctx mut TraverseCtx<'a>, +} + +impl<'a, 'ctx> ConstructorParamsSuperReplacer<'a, 'ctx> { + fn new(ctx: &'ctx mut TraverseCtx<'a>) -> Self { + Self { super_binding: None, ctx } + } + + /// Replace `super()` in constructor params with `_super().call(super())`. + /// + /// If not found in params, returns `None`. + /// + /// If it is found, also replaces any `super()` calls in constructor body. + fn replace( + mut self, + constructor: &mut Function<'a>, + ) -> Option<(ScopeId, InstanceInitsInsertLocation<'a>)> { + self.visit_formal_parameters(&mut constructor.params); + + #[expect(clippy::question_mark)] + if self.super_binding.is_none() { + // No `super()` in constructor params + return None; + } + + // `super()` was found in constructor params. + // Replace any `super()`s in constructor body with `_super.call(super())`. + // TODO: Is this correct if super class constructor returns another object? + // ```js + // class S { constructor() { return {}; } } + // class C extends S { prop = 1; constructor(x = super()) {} } + // ``` + let body_stmts = &mut constructor.body.as_mut().unwrap().statements; + self.visit_statements(body_stmts); + + let super_binding = self.super_binding.unwrap(); + let insert_location = InstanceInitsInsertLocation::SuperFnOutsideClass(super_binding); + + // Create scope for `_super` function + let outer_scope_id = self.ctx.current_block_scope_id(); + let super_func_scope_id = self.ctx.scoping_mut().add_scope( + Some(outer_scope_id), + NodeId::DUMMY, + ScopeFlags::Function | ScopeFlags::StrictMode, + ); + + Some((super_func_scope_id, insert_location)) + } +} + +impl<'a> VisitMut<'a> for ConstructorParamsSuperReplacer<'a, '_> { + /// Replace `super()` with `_super.call(super())`. + // `#[inline]` to make hot path for all other expressions as cheap as possible. + #[inline] + fn visit_expression(&mut self, expr: &mut Expression<'a>) { + if let Expression::CallExpression(call_expr) = expr + && call_expr.callee.is_super() + { + // Walk `CallExpression`'s arguments here rather than falling through to `walk_expression` + // below to avoid infinite loop as `super()` gets visited over and over + self.visit_arguments(&mut call_expr.arguments); + + let span = call_expr.span; + self.wrap_super(expr, span); + return; + } + + walk_mut::walk_expression(self, expr); + } + + // Stop traversing where scope of current `super` ends + #[inline] + fn visit_function(&mut self, _func: &mut Function<'a>, _flags: ScopeFlags) {} + + #[inline] + fn visit_static_block(&mut self, _block: &mut StaticBlock) {} + + #[inline] + fn visit_ts_module_block(&mut self, _block: &mut TSModuleBlock<'a>) {} + + #[inline] + fn visit_property_definition(&mut self, prop: &mut PropertyDefinition<'a>) { + // `super()` in computed key of property or method refers to super binding of parent class. + // So visit computed `key`, but not `value`. + // ```js + // class Outer extends OuterSuper { + // constructor( + // x = class Inner extends InnerSuper { + // [super().foo] = 1; // `super()` refers to `Outer`'s super class + // [super().bar]() {} // `super()` refers to `Outer`'s super class + // x = super(); // `super()` refers to `Inner`'s super class, but illegal syntax + // } + // ) {} + // } + // ``` + // Don't visit `type_annotation` field because can't contain `super()`. + // TODO: Are decorators in scope? + self.visit_decorators(&mut prop.decorators); + if prop.computed { + self.visit_property_key(&mut prop.key); + } + } + + #[inline] + fn visit_accessor_property(&mut self, prop: &mut AccessorProperty<'a>) { + // Visit computed `key` but not `value`, for same reasons as `visit_property_definition` above. + // TODO: Are decorators in scope? + self.visit_decorators(&mut prop.decorators); + if prop.computed { + self.visit_property_key(&mut prop.key); + } + } +} + +impl<'a> ConstructorParamsSuperReplacer<'a, '_> { + /// Wrap `super()` -> `_super.call(super())` + fn wrap_super(&mut self, expr: &mut Expression<'a>, span: Span) { + let super_binding = self.super_binding.get_or_insert_with(|| { + self.ctx.generate_uid( + "super", + self.ctx.current_block_scope_id(), + SymbolFlags::BlockScopedVariable, + ) + }); + + let ctx = &mut *self.ctx; + let super_call = expr.take_in(ctx.ast); + *expr = ctx.ast.expression_call( + span, + Expression::from(ctx.ast.member_expression_static( + SPAN, + super_binding.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, Atom::from("call")), + false, + )), + NONE, + ctx.ast.vec1(Argument::from(super_call)), + false, + ); + } +} + +/// Visitor for transforming `super()` in class constructor body. +struct ConstructorBodySuperReplacer<'a, 'ctx> { + /// Scope of class constructor + constructor_scope_id: ScopeId, + /// Binding for `_super` function. + /// Initially `None`. Binding is created if `super()` is found in position other than top-level, + /// that requires a `_super` function. + super_binding: Option>, + ctx: &'ctx mut TraverseCtx<'a>, +} + +impl<'a, 'ctx> ConstructorBodySuperReplacer<'a, 'ctx> { + fn new(constructor_scope_id: ScopeId, ctx: &'ctx mut TraverseCtx<'a>) -> Self { + Self { constructor_scope_id, super_binding: None, ctx } + } + + /// If `super()` found first as a top level statement (`constructor() { let x; super(); }`), + /// does not alter constructor, and returns `InstanceInitsInsertLocation::ExistingConstructor` + /// and constructor's `ScopeId`. + /// + /// Otherwise, replaces any `super()` calls with `_super()` and returns + /// `InstanceInitsInsertLocation::SuperFnInsideConstructor`, and `ScopeId` for `_super` function. + fn replace( + mut self, + constructor: &mut Function<'a>, + ) -> (ScopeId, InstanceInitsInsertLocation<'a>) { + // This is not a real loop. It always breaks on 1st iteration. + // Only here so that can break out of it from within inner `for` loop. + #[expect(clippy::never_loop)] + 'outer: loop { + let body_stmts = &mut constructor.body.as_mut().unwrap().statements; + for (index, stmt) in body_stmts.iter_mut().enumerate() { + // If statement is standalone `super()`, insert inits after `super()`. + // We can avoid a `_super` function for this common case. + if let Statement::ExpressionStatement(expr_stmt) = stmt + && let Expression::CallExpression(call_expr) = &mut expr_stmt.expression + && let Expression::Super(super_) = &call_expr.callee + { + let span = super_.span; + + // Visit arguments in `super(x, y, z)` call. + // Required to handle edge case `super(self = super())`. + self.visit_arguments(&mut call_expr.arguments); + + // Found `super()` as top-level statement + if self.super_binding.is_none() { + // This is the first `super()` found + // (and no further `super()` calls within `super()` call's arguments). + // So can just insert initializers after it - no need for `_super` function. + let insert_location = + InstanceInitsInsertLocation::ExistingConstructor(index + 1); + return (self.constructor_scope_id, insert_location); + } + + // `super()` was previously found in nested position before this. + // So we do need a `_super` function. + // But we don't need to look any further for any other `super()` calls, + // because calling `super()` after this would be an immediate error. + self.replace_super(call_expr, span); + + break 'outer; + } + + // Traverse statement looking for `super()` deeper in the statement + self.visit_statement(stmt); + } + + if self.super_binding.is_none() { + // No `super()` anywhere in constructor. + // This is weird, but legal code. It would be a runtime error if the class is constructed + // (unless the constructor returns early). + // In reasonable code, we should never get here. + // Handle this weird case of no `super()` by inserting initializers in a `_super` function + // which is never called. That is pointless, but not inserting the initializers anywhere + // would leave `Semantic` in an inconsistent state. + // What we get is completely legal output and correct `Semantic`, just longer than it + // could be. But this should very rarely happen in practice, and minifier will delete + // the `_super` function as dead code. + // So set `super_binding` and exit the loop, so it's treated as if `super()` was found + // in a nested position. + // TODO: Delete the initializers instead. + self.super_binding = Some(self.create_super_binding()); + } + + break; + } + + let super_func_scope_id = self.ctx.scoping_mut().add_scope( + Some(self.constructor_scope_id), + NodeId::DUMMY, + ScopeFlags::Function | ScopeFlags::Arrow | ScopeFlags::StrictMode, + ); + let super_binding = self.super_binding.unwrap(); + let insert_location = InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding); + (super_func_scope_id, insert_location) + } +} + +impl<'a> VisitMut<'a> for ConstructorBodySuperReplacer<'a, '_> { + /// Replace `super()` with `_super()`. + // `#[inline]` to make hot path for all other function calls as cheap as possible. + #[inline] + fn visit_call_expression(&mut self, call_expr: &mut CallExpression<'a>) { + if let Expression::Super(super_) = &call_expr.callee { + let span = super_.span; + self.replace_super(call_expr, span); + } + + walk_mut::walk_call_expression(self, call_expr); + } + + // Stop traversing where scope of current `super` ends + #[inline] + fn visit_function(&mut self, _func: &mut Function<'a>, _flags: ScopeFlags) {} + + #[inline] + fn visit_static_block(&mut self, _block: &mut StaticBlock) {} + + #[inline] + fn visit_ts_module_block(&mut self, _block: &mut TSModuleBlock<'a>) {} + + #[inline] + fn visit_property_definition(&mut self, prop: &mut PropertyDefinition<'a>) { + // `super()` in computed key of property or method refers to super binding of parent class. + // So visit computed `key`, but not `value`. + // ```js + // class Outer extends OuterSuper { + // constructor() { + // class Inner extends InnerSuper { + // [super().foo] = 1; // `super()` refers to `Outer`'s super class + // [super().bar]() {} // `super()` refers to `Outer`'s super class + // x = super(); // `super()` refers to `Inner`'s super class, but illegal syntax + // } + // } + // } + // ``` + // Don't visit `type_annotation` field because can't contain `super()`. + // TODO: Are decorators in scope? + self.visit_decorators(&mut prop.decorators); + if prop.computed { + self.visit_property_key(&mut prop.key); + } + } + + #[inline] + fn visit_accessor_property(&mut self, prop: &mut AccessorProperty<'a>) { + // Visit computed `key` but not `value`, for same reasons as `visit_property_definition` above. + // TODO: Are decorators in scope? + self.visit_decorators(&mut prop.decorators); + if prop.computed { + self.visit_property_key(&mut prop.key); + } + } +} + +impl<'a> ConstructorBodySuperReplacer<'a, '_> { + /// Replace `super(arg1, arg2)` with `_super(arg1, arg2)` + fn replace_super(&mut self, call_expr: &mut CallExpression<'a>, span: Span) { + if self.super_binding.is_none() { + self.super_binding = Some(self.create_super_binding()); + } + let super_binding = self.super_binding.as_ref().unwrap(); + + call_expr.callee = super_binding.create_spanned_read_expression(span, self.ctx); + } + + /// Create binding for `_super` function + fn create_super_binding(&mut self) -> BoundIdentifier<'a> { + self.ctx.generate_uid( + "super", + self.constructor_scope_id, + SymbolFlags::FunctionScopedVariable, + ) + } +} + +/// Visitor to rename bindings and references. +struct ConstructorSymbolRenamer<'a, 'v> { + clashing_symbols: &'v mut FxHashMap>, + ctx: &'v TraverseCtx<'a>, +} + +impl<'a, 'v> ConstructorSymbolRenamer<'a, 'v> { + fn new( + clashing_symbols: &'v mut FxHashMap>, + ctx: &'v TraverseCtx<'a>, + ) -> Self { + Self { clashing_symbols, ctx } + } +} + +impl<'a> VisitMut<'a> for ConstructorSymbolRenamer<'a, '_> { + fn visit_binding_identifier(&mut self, ident: &mut BindingIdentifier<'a>) { + let symbol_id = ident.symbol_id(); + if let Some(new_name) = self.clashing_symbols.get(&symbol_id) { + ident.name = *new_name; + } + } + + fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) { + let reference_id = ident.reference_id(); + if let Some(symbol_id) = self.ctx.scoping().get_reference(reference_id).symbol_id() + && let Some(new_name) = self.clashing_symbols.get(&symbol_id) + { + ident.name = *new_name; + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/instance_prop_init.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/instance_prop_init.rs new file mode 100644 index 000000000000..46ee6462e8dd --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/instance_prop_init.rs @@ -0,0 +1,230 @@ +//! ES2022: Class Properties +//! Transform of instance property initializers. + +use std::cell::Cell; + +use rustc_hash::FxHashMap; + +use oxc_ast::ast::*; +use oxc_ast_visit::Visit; +use oxc_data_structures::stack::Stack; +use oxc_span::Atom; +use oxc_syntax::{ + scope::{ScopeFlags, ScopeId}, + symbol::SymbolId, +}; + +use crate::context::TraverseCtx; + +use super::ClassProperties; + +impl<'a> ClassProperties<'a, '_> { + /// Reparent property initializers scope. + /// + /// Instance property initializers move from the class body into either class constructor, + /// or a `_super` function. Change parent scope of first-level scopes in initializer to reflect this. + pub(super) fn reparent_initializers_scope( + &mut self, + inits: &[Expression<'a>], + instance_inits_scope_id: ScopeId, + instance_inits_constructor_scope_id: Option, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(constructor_scope_id) = instance_inits_constructor_scope_id { + // Re-parent first-level scopes, and check for symbol clashes + let mut updater = InstanceInitializerVisitor::new( + instance_inits_scope_id, + constructor_scope_id, + self, + ctx, + ); + for init in inits { + updater.visit_expression(init); + } + } else { + // No symbol clashes possible. Just re-parent first-level scopes (faster). + let mut updater = FastInstanceInitializerVisitor::new(instance_inits_scope_id, ctx); + for init in inits { + updater.visit_expression(init); + } + } + } +} + +/// Visitor to change parent scope of first-level scopes in instance property initializer, +/// and find any `IdentifierReference`s which would be shadowed by bindings in constructor, +/// once initializer moves into constructor body. +struct InstanceInitializerVisitor<'a, 'v> { + /// Pushed to when entering a scope, popped when exiting it. + /// Parent `ScopeId` should be updated when stack is empty (i.e. this is a first-level scope). + scope_ids_stack: Stack, + /// New parent scope for first-level scopes in initializer + parent_scope_id: ScopeId, + /// Constructor's scope, for checking symbol clashes against + constructor_scope_id: ScopeId, + /// Clashing symbols + clashing_symbols: &'v mut FxHashMap>, + /// `TransCtx` object. + ctx: &'v mut TraverseCtx<'a>, +} + +impl<'a, 'v> InstanceInitializerVisitor<'a, 'v> { + fn new( + instance_inits_scope_id: ScopeId, + constructor_scope_id: ScopeId, + class_properties: &'v mut ClassProperties<'a, '_>, + ctx: &'v mut TraverseCtx<'a>, + ) -> Self { + Self { + // Most initializers don't contain any scopes, so best default is 0 capacity + // to avoid an allocation in most cases + scope_ids_stack: Stack::new(), + parent_scope_id: instance_inits_scope_id, + constructor_scope_id, + clashing_symbols: &mut class_properties.clashing_constructor_symbols, + ctx, + } + } +} + +impl<'a> Visit<'a> for InstanceInitializerVisitor<'a, '_> { + /// Update parent scope for first level of scopes. + /// Convert scope to sloppy mode if `self.make_sloppy_mode == true`. + // + // `#[inline]` because this function is small and called from many `walk` functions + #[inline] + fn enter_scope(&mut self, _flags: ScopeFlags, scope_id: &Cell>) { + let scope_id = scope_id.get().unwrap(); + if self.scope_ids_stack.is_empty() { + self.reparent_scope(scope_id); + } + self.scope_ids_stack.push(scope_id); + } + + // `#[inline]` because this function is tiny and called from many `walk` functions + #[inline] + fn leave_scope(&mut self) { + self.scope_ids_stack.pop(); + } + + // `#[inline]` because this function just delegates to `check_for_symbol_clash` + #[inline] + fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) { + self.check_for_symbol_clash(ident); + } +} + +impl<'a> InstanceInitializerVisitor<'a, '_> { + /// Update parent of scope. + fn reparent_scope(&mut self, scope_id: ScopeId) { + self.ctx.scoping_mut().change_scope_parent_id(scope_id, Some(self.parent_scope_id)); + } + + /// Check if symbol referenced by `ident` is shadowed by a binding in constructor's scope. + fn check_for_symbol_clash(&mut self, ident: &IdentifierReference<'a>) { + // TODO: It would be ideal if could get reference `&Bindings` for constructor + // in `InstanceInitializerVisitor::new` rather than indexing into `ScopeTree::bindings` + // with same `ScopeId` every time here, but `ScopeTree` doesn't allow that, and we also + // take a `&mut ScopeTree` in `reparent_scope`, so borrow-checker doesn't allow that. + let Some(constructor_symbol_id) = + self.ctx.scoping().get_binding(self.constructor_scope_id, &ident.name) + else { + return; + }; + + // Check the symbol this identifier refers to is bound outside of the initializer itself. + // If it's bound within the initializer, there's no clash, so exit. + // e.g. `class C { double = (n) => n * 2; constructor(n) {} }` + // Even though there's a binding `n` in constructor, it doesn't shadow the use of `n` in init. + // This is an improvement over Babel. + let reference_id = ident.reference_id(); + if let Some(ident_symbol_id) = self.ctx.scoping().get_reference(reference_id).symbol_id() { + let scope_id = self.ctx.scoping().symbol_scope_id(ident_symbol_id); + if self.scope_ids_stack.contains(&scope_id) { + return; + } + } + + // Record the symbol clash. Symbol in constructor needs to be renamed. + self.clashing_symbols.entry(constructor_symbol_id).or_insert(ident.name); + } +} + +/// Visitor to change parent scope of first-level scopes in instance property initializer. +/// +/// Unlike `InstanceInitializerVisitor`, does not check for symbol clashes. +/// +/// Therefore only needs to walk until find a node which has a scope. No point continuing to traverse +/// inside that scope, as by definition any nested scopes can't be first level. +/// +/// The visitors here are for the only types which can be the first scope reached when starting +/// traversal from an `Expression`. +struct FastInstanceInitializerVisitor<'a, 'v> { + /// Parent scope + parent_scope_id: ScopeId, + /// `TransCtx` object. + ctx: &'v mut TraverseCtx<'a>, +} + +impl<'a, 'v> FastInstanceInitializerVisitor<'a, 'v> { + fn new(instance_inits_scope_id: ScopeId, ctx: &'v mut TraverseCtx<'a>) -> Self { + Self { parent_scope_id: instance_inits_scope_id, ctx } + } +} + +impl<'a> Visit<'a> for FastInstanceInitializerVisitor<'a, '_> { + #[inline] + fn visit_function(&mut self, func: &Function<'a>, _flags: ScopeFlags) { + self.reparent_scope(&func.scope_id); + } + + #[inline] + fn visit_arrow_function_expression(&mut self, func: &ArrowFunctionExpression<'a>) { + self.reparent_scope(&func.scope_id); + } + + #[inline] + fn visit_class(&mut self, class: &Class<'a>) { + // `decorators` is outside `Class`'s scope and can contain scopes itself + self.visit_decorators(&class.decorators); + + self.reparent_scope(&class.scope_id); + } + + #[inline] + fn visit_ts_conditional_type(&mut self, conditional: &TSConditionalType<'a>) { + // `check_type` is outside `TSConditionalType`'s scope and can contain scopes itself + self.visit_ts_type(&conditional.check_type); + + self.reparent_scope(&conditional.scope_id); + + // `false_type` is outside `TSConditionalType`'s scope and can contain scopes itself + self.visit_ts_type(&conditional.false_type); + } + + #[inline] + fn visit_ts_method_signature(&mut self, signature: &TSMethodSignature<'a>) { + self.reparent_scope(&signature.scope_id); + } + + #[inline] + fn visit_ts_construct_signature_declaration( + &mut self, + signature: &TSConstructSignatureDeclaration<'a>, + ) { + self.reparent_scope(&signature.scope_id); + } + + #[inline] + fn visit_ts_mapped_type(&mut self, mapped: &TSMappedType<'a>) { + self.reparent_scope(&mapped.scope_id); + } +} + +impl FastInstanceInitializerVisitor<'_, '_> { + /// Update parent of scope. + fn reparent_scope(&mut self, scope_id: &Cell>) { + let scope_id = scope_id.get().unwrap(); + self.ctx.scoping_mut().change_scope_parent_id(scope_id, Some(self.parent_scope_id)); + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/mod.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/mod.rs new file mode 100644 index 000000000000..a3e92dac64c1 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/mod.rs @@ -0,0 +1,433 @@ +//! 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; +//! method() { +//! let bar = this.#bar; +//! this.#bar = bar + 1; +//! } +//! } +//! +//! 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); +//! } +//! method() { +//! let bar = babelHelpers.classPrivateFieldGet2(_bar, this); +//! babelHelpers.classPrivateFieldSet2(_bar, this, bar + 1); +//! } +//! } +//! +//! 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); +//! } +//! } +//! } +//! ``` +//! +//! ## Options +//! +//! ### `loose` +//! +//! This option can also be enabled with `CompilerAssumptions::set_public_class_fields`. +//! +//! When `true`, class properties are compiled to use an assignment expression instead of +//! `_defineProperty` helper. +//! +//! #### Example +//! +//! Input: +//! ```js +//! class C { +//! foo = 123; +//! } +//! ``` +//! +//! With `loose: false` (default): +//! +//! ```js +//! class C { +//! constructor() { +//! babelHelpers.defineProperty(this, "foo", 123); +//! } +//! } +//! ``` +//! +//! With `loose: true`: +//! +//! ```js +//! class C { +//! constructor() { +//! this.foo = 123; +//! } +//! } +//! ``` +//! +//! ## Implementation +//! +//! ### Reference implementation +//! +//! Implementation based on [@babel/plugin-transform-class-properties](https://babel.dev/docs/babel-plugin-transform-class-properties). +//! +//! I (@overlookmotel) wrote this transform without reference to Babel's internal implementation, +//! but aiming to reproduce Babel's output, guided by Babel's test suite. +//! +//! ### Divergence from Babel +//! +//! In a few places, our implementation diverges from Babel, notably inserting property initializers +//! into constructor of a class with multiple `super()` calls (see comments in [`constructor`] module). +//! +//! ### High level overview +//! +//! Transform happens in 3 phases: +//! +//! 1. On entering class body: +//! ([`ClassProperties::transform_class_body_on_entry`]) +//! * Check if class contains properties or static blocks, to determine if any transform is necessary. +//! Exit if nothing to do. +//! * Build a hashmap of private property keys. +//! * Extract instance property initializers (public or private) from class body and insert into +//! class constructor. +//! * Temporarily replace computed keys of instance properties with assignments to temp vars. +//! `class C { [foo()] = 123; }` -> `class C { [_foo = foo()]; }` +//! +//! 2. During traversal of class body: +//! ([`ClassProperties::transform_private_field_expression`] and other visitors) +//! * Transform private fields (`this.#foo`). +//! +//! 3. On exiting class: +//! ([`ClassProperties::transform_class_declaration_on_exit`] and [`ClassProperties::transform_class_expression_on_exit`]) +//! * Transform static properties, and static blocks. +//! * Move assignments to temp vars which were inserted in computed keys for in phase 1 to before class. +//! * Create temp vars for computed method keys if required. +//! * Insert statements before/after class declaration / expressions before/after class expression. +//! +//! The reason for doing transform in 3 phases is that everything needs to stay within the class body +//! while main traverse executes, so that other transforms have a chance to run on that code. +//! +//! Static property initializers, static blocks, and computed keys move to outside the class eventually, +//! but we move them in the final exit phase, so they get transformed first. +//! Additionally, any private fields (`this.#prop`) in these parts are also transformed in the main traverse +//! by this transform. +//! +//! However, we can't leave *everything* until the exit phase because: +//! +//! 1. We need to compile a list of private properties before main traversal. +//! 2. Instance property initializers need to move into the class constructor, and if we don't do that +//! before the main traversal of class body, then other transforms running on instance property +//! initializers will create temp vars outside the class, when they should be in constructor. +//! +//! Note: We execute the entry phase on entering class *body*, not class, because private properties +//! defined in a class only affect the class body, and not the `extends` clause. +//! By only pushing details of the class to the stack when entering class *body*, we avoid any class +//! fields in the `extends` clause being incorrectly resolved to private properties defined in that class, +//! as `extends` clause is visited before class body. +//! +//! ### Structures +//! +//! Transform stores 2 sets of state: +//! +//! 1. Details about classes in a stack of `ClassDetails` - `classes_stack`. +//! This stack is pushed to when entering class body, and popped when exiting class. +//! This contains data which is used in both the enter and exit phases. +//! 2. A set of properties - `insert_before` etc. +//! These properties are only used in *either* enter or exit phase. +//! State cannot be shared between enter and exit phases in these properties, as they'll get clobbered +//! if there's a nested class within this one. +//! +//! We don't store all state in `ClassDetails` as a performance optimization. +//! It reduces the size of `ClassDetails` which has be repeatedly pushed and popped from stack, +//! and allows reusing same `Vec`s and `FxHashMap`s for each class, rather than creating new each time. +//! +//! ### Files +//! +//! Implementation is split into several files: +//! +//! * `mod.rs`: Setup and visitor. +//! * `class.rs`: Transform of class body. +//! * `prop_decl.rs`: Transform of property declarations (instance and static). +//! * `constructor.rs`: Insertion of property initializers into class constructor. +//! * `instance_prop_init.rs`: Transform of instance property initializers. +//! * `static_block_and_prop_init.rs`: Transform of static property initializers and static blocks. +//! * `computed_key.rs`: Transform of property/method computed keys. +//! * `private_field.rs`: Transform of private fields (`this.#prop`). +//! * `private_method.rs`: Transform of private methods (`this.#method()`). +//! * `super_converter.rs`: Transform `super` expressions. +//! * `class_details.rs`: Structures containing details of classes and private properties. +//! * `class_bindings.rs`: Structure containing bindings for class name and temp var. +//! * `utils.rs`: Utility functions. +//! +//! ## References +//! +//! * Babel plugin implementation: +//! * +//! * +//! * +//! * Class properties TC39 proposal: + +use indexmap::IndexMap; +use rustc_hash::{FxBuildHasher, FxHashMap}; +use serde::Deserialize; + +use oxc_ast::ast::*; +use oxc_span::Atom; +use oxc_syntax::symbol::SymbolId; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +mod class; +mod class_bindings; +mod class_details; +mod computed_key; +mod constructor; +mod instance_prop_init; +mod private_field; +mod private_method; +mod prop_decl; +mod static_block_and_prop_init; +mod super_converter; +mod utils; +use class_bindings::ClassBindings; +use class_details::{ClassDetails, ClassesStack, PrivateProp, ResolvedPrivateProp}; + +type FxIndexMap = IndexMap; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct ClassPropertiesOptions { + pub loose: bool, +} + +/// Class properties transform. +/// +/// See [module docs] for details. +/// +/// [module docs]: self +pub struct ClassProperties<'a, 'ctx> { + // ----- Options ----- + // + /// If `true`, set properties with `=`, instead of `_defineProperty` helper (loose option). + set_public_class_fields: bool, + /// If `true`, store private properties as normal properties as string keys (loose option). + private_fields_as_properties: bool, + /// If `true`, transform static blocks. + transform_static_blocks: bool, + /// If `true`, remove class fields without initializer. Only works with `set_public_class_fields: true`. + /// + /// This option is controlled by [`crate::TypeScriptOptions::remove_class_fields_without_initializer`]. + remove_class_fields_without_initializer: bool, + + ctx: &'ctx TransformCtx<'a>, + + // ----- State used during all phases of transform ----- + // + /// Stack of classes. + /// Pushed to when entering a class, popped when exiting. + /// + /// The way stack is used is not perfect, because pushing to/popping from it in + /// `enter_class_body` / `exit_expression`. If another transform replaces/removes the class + /// in an earlier `exit_expression` visitor, then stack will get out of sync. + /// I (@overlookmotel) don't think there's a solution to this, and I don't think any other + /// transforms will remove a class expression in this way, so should be OK. + /// This problem only affects class expressions. Class declarations aren't affected, + /// as their exit-phase transform happens in `exit_class`. + classes_stack: ClassesStack<'a>, + /// Count of private fields in current class and parent classes. + private_field_count: usize, + + // ----- State used only during enter phase ----- + // + /// Symbols in constructor which clash with instance prop initializers. + /// Keys are symbols' IDs. + /// Values are initially the original name of binding, later on the name of new UID name. + clashing_constructor_symbols: FxHashMap>, + + // ----- State used only during exit phase ----- + // + /// Expressions to insert before class + insert_before: Vec>, + /// Expressions to insert after class expression + insert_after_exprs: Vec>, + /// Statements to insert after class declaration + insert_after_stmts: Vec>, +} + +impl<'a, 'ctx> ClassProperties<'a, 'ctx> { + /// Create `ClassProperties` transformer + pub fn new( + options: ClassPropertiesOptions, + transform_static_blocks: bool, + remove_class_fields_without_initializer: bool, + ctx: &'ctx TransformCtx<'a>, + ) -> Self { + // TODO: Raise error if these 2 options are inconsistent + let set_public_class_fields = options.loose || ctx.assumptions.set_public_class_fields; + // TODO: Raise error if these 2 options are inconsistent + let private_fields_as_properties = + options.loose || ctx.assumptions.private_fields_as_properties; + + Self { + set_public_class_fields, + private_fields_as_properties, + transform_static_blocks, + remove_class_fields_without_initializer, + ctx, + classes_stack: ClassesStack::new(), + private_field_count: 0, + // `Vec`s and `FxHashMap`s which are reused for every class being transformed + clashing_constructor_symbols: FxHashMap::default(), + insert_before: vec![], + insert_after_exprs: vec![], + insert_after_stmts: vec![], + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ClassProperties<'a, '_> { + #[expect(clippy::inline_always)] + #[inline(always)] // Because this is a no-op in release mode + fn exit_program(&mut self, _program: &mut Program<'a>, _ctx: &mut TraverseCtx<'a>) { + debug_assert_eq!(self.private_field_count, 0); + } + + fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { + self.transform_class_body_on_entry(body, ctx); + } + + fn exit_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.transform_class_declaration_on_exit(class, ctx); + } + + // `#[inline]` for fast exit for expressions which are not `Class`es + #[inline] + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if matches!(expr, Expression::ClassExpression(_)) { + self.transform_class_expression_on_exit(expr, ctx); + } + } + + // `#[inline]` for fast exit for expressions which are not any of the transformed types + #[inline] + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + // All of transforms below only act on `PrivateFieldExpression`s or `PrivateInExpression`s. + // If we're not inside a class which has private fields, `#prop` can't be present here, + // so exit early - fast path for common case. + if self.private_field_count == 0 { + return; + } + + match expr { + // `object.#prop` + Expression::PrivateFieldExpression(_) => { + self.transform_private_field_expression(expr, ctx); + } + // `object.#prop()` + Expression::CallExpression(_) => { + self.transform_call_expression(expr, ctx); + } + // `object.#prop = value`, `object.#prop += value`, `object.#prop ??= value` etc + Expression::AssignmentExpression(_) => { + self.transform_assignment_expression(expr, ctx); + } + // `object.#prop++`, `--object.#prop` + Expression::UpdateExpression(_) => { + self.transform_update_expression(expr, ctx); + } + // `object?.#prop` + Expression::ChainExpression(_) => { + self.transform_chain_expression(expr, ctx); + } + // `delete object?.#prop.xyz` + Expression::UnaryExpression(_) => { + self.transform_unary_expression(expr, ctx); + } + // "object.#prop`xyz`" + Expression::TaggedTemplateExpression(_) => { + self.transform_tagged_template_expression(expr, ctx); + } + // "#prop in object" + Expression::PrivateInExpression(_) => { + self.transform_private_in_expression(expr, ctx); + } + _ => {} + } + } + + // `#[inline]` for fast exit for assignment targets which are not private fields (rare case) + #[inline] + fn enter_assignment_target( + &mut self, + target: &mut AssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.transform_assignment_target(target, ctx); + } + + fn enter_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + // Ignore `declare` properties as they don't have any runtime effect, + // and will be removed in the TypeScript transform later + if prop.r#static && !prop.declare { + self.flag_entering_static_property_or_block(); + } + } + + fn exit_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + // Ignore `declare` properties as they don't have any runtime effect, + // and will be removed in the TypeScript transform later + if prop.r#static && !prop.declare { + self.flag_exiting_static_property_or_block(); + } + } + + fn enter_static_block(&mut self, _block: &mut StaticBlock<'a>, _ctx: &mut TraverseCtx<'a>) { + self.flag_entering_static_property_or_block(); + } + + fn exit_static_block(&mut self, _block: &mut StaticBlock<'a>, _ctx: &mut TraverseCtx<'a>) { + self.flag_exiting_static_property_or_block(); + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/private_field.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/private_field.rs new file mode 100644 index 000000000000..dde66433ce7c --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/private_field.rs @@ -0,0 +1,2225 @@ +//! ES2022: Class Properties +//! Transform of private property uses e.g. `this.#prop`. + +use std::mem; + +use oxc_allocator::{Box as ArenaBox, TakeIn}; +use oxc_ast::{NONE, ast::*}; +use oxc_span::SPAN; +use oxc_syntax::{reference::ReferenceId, symbol::SymbolId}; +use oxc_traverse::{Ancestor, BoundIdentifier, ast_operations::get_var_name_from_node}; + +use crate::{ + common::helper_loader::Helper, + context::{TransformCtx, TraverseCtx}, + utils::ast_builder::{ + create_assignment, create_bind_call, create_call_call, create_member_callee, + }, +}; + +use super::{ + ClassProperties, ResolvedPrivateProp, + class_details::ResolvedGetSetPrivateProp, + utils::{ + create_underscore_ident_name, debug_assert_expr_is_not_parenthesis_or_typescript_syntax, + }, +}; + +impl<'a> ClassProperties<'a, '_> { + /// Transform private field expression. + /// + /// Not loose: + /// * Instance prop: `object.#prop` -> `_classPrivateFieldGet2(_prop, object)` + /// * Static prop: `object.#prop` -> `_assertClassBrand(Class, object, _prop)._` + /// * Instance method: `object.#method` -> `_assertClassBrand(_Class_brand, object, _prop)` + /// * Static method: `object.#method` -> `_assertClassBrand(Class, object, _prop)` + /// * Instance getter: `object.#getter` -> `get_getter.call(_assertClassBrand(_Class_brand, object))` + /// * Static getter: `object.#getter` -> `get_getter.call(_assertClassBrand(Class, object))` + /// * Instance setter: `object.#setter` -> `set_setter.bind(_assertClassBrand(_Class_brand, object))` + /// * Static setter: `object.#setter` -> `set_setter.bind(_assertClassBrand(Class, object))` + /// + /// Loose: `object.#prop` -> `_classPrivateFieldLooseBase(object, _prop)[_prop]` + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::PrivateFieldExpression`. + #[inline] + pub(super) fn transform_private_field_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::PrivateFieldExpression(field_expr) = expr else { unreachable!() }; + *expr = self.transform_private_field_expression_impl(field_expr, false, ctx); + } + + fn transform_private_field_expression_impl( + &mut self, + field_expr: &mut PrivateFieldExpression<'a>, + is_assignment: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let span = field_expr.span; + let object = field_expr.object.take_in(ctx.ast); + let resolved = if is_assignment { + match self.classes_stack.find_writeable_private_prop(&field_expr.field) { + Some(prop) => prop, + _ => { + // Early return for read-only error + return self.create_sequence_with_read_only_error( + &field_expr.field.name, + object, + None, + span, + ctx, + ); + } + } + } else { + match self.classes_stack.find_readable_private_prop(&field_expr.field) { + Some(prop) => prop, + _ => { + // Early return for write-only error + return self.create_sequence_with_write_only_error( + &field_expr.field.name, + object, + span, + ctx, + ); + } + } + }; + + let ResolvedPrivateProp { + prop_binding, + class_bindings, + is_static, + is_method, + is_accessor, + is_declaration, + } = resolved; + + if self.private_fields_as_properties { + // `_classPrivateFieldLooseBase(object, _prop)[_prop]` + return Expression::from(Self::create_private_field_member_expr_loose( + object, + prop_binding, + span, + self.ctx, + ctx, + )); + } + + let prop_ident = prop_binding.create_read_expression(ctx); + + if is_static { + // TODO: Ensure there are tests for nested classes with references to private static props + // of outer class inside inner class, to make sure we're getting the right `class_bindings`. + + // If `object` is reference to class name, there's no need for the class brand assertion + if let Some((class_symbol_id, object_reference_id)) = Self::shortcut_static_class( + is_declaration, + class_bindings.name_symbol_id(), + &object, + ctx, + ) { + if is_method { + if is_assignment { + // `toSetter(_prop.bind(object), [])._` + self.create_to_setter_for_bind_call(prop_ident, object, span, ctx) + } else if is_accessor { + // `_prop.call(object)` + create_call_call(prop_ident, object, span, ctx) + } else { + ctx.scoping_mut() + .delete_resolved_reference(class_symbol_id, object_reference_id); + // `_prop` + prop_ident + } + } else { + ctx.scoping_mut() + .delete_resolved_reference(class_symbol_id, object_reference_id); + // `_prop._` + Self::create_underscore_member_expression(prop_ident, span, ctx) + } + } else { + // `_assertClassBrand(Class, object, _prop)._` + let class_binding = class_bindings.get_or_init_static_binding(ctx); + let class_ident = class_binding.create_read_expression(ctx); + if is_method { + if is_assignment { + // `toSetter(_prop.bind(object), [])._` + let object = + self.create_assert_class_brand_without_value(class_ident, object, ctx); + self.create_to_setter_for_bind_call(prop_ident, object, span, ctx) + } else if is_accessor { + // `_prop.bind(_assertClassBrand(Class, object))` + let object = + self.create_assert_class_brand_without_value(class_ident, object, ctx); + create_call_call(prop_ident, object, span, ctx) + } else { + self.create_assert_class_brand(class_ident, object, prop_ident, span, ctx) + } + } else { + self.create_assert_class_brand_underscore( + class_ident, + object, + prop_ident, + span, + ctx, + ) + } + } + } else if is_method { + let brand_ident = class_bindings.brand().create_read_expression(ctx); + if is_assignment { + // `_toSetter(_prop.call(_assertClassBrand(_Class_brand, object)))._` + let object = self.create_assert_class_brand_without_value(brand_ident, object, ctx); + self.create_to_setter_for_bind_call(prop_ident, object, span, ctx) + } else if is_accessor { + // `_prop.bind(_assertClassBrand(_Class_brand, object))` + let object = self.create_assert_class_brand_without_value(brand_ident, object, ctx); + create_call_call(prop_ident, object, span, ctx) + } else { + self.create_assert_class_brand(brand_ident, object, prop_ident, span, ctx) + } + } else if is_assignment { + // `_toSetter(_classPrivateFieldSet2, [_prop, object])._` + self.create_to_setter_for_private_field_set(prop_ident, object, span, ctx) + } else { + // `_classPrivateFieldGet2(_prop, object)` + self.create_private_field_get(prop_ident, object, span, ctx) + } + } + + /// Check if can use shorter version of static private prop transform. + /// + /// Can if all of: + /// 1. Class is a declaration, not an expression. + /// 2. Class has a name. + /// 3. `object` is an `IdentifierReference` referring to class name binding. + /// + /// If can use shorter version, returns `SymbolId` and `ReferenceId` of the `IdentifierReference`. + // + // TODO(improve-on-babel): No reason not to use the short version for class expressions too. + // TODO: Take `&ClassBindings` instead of `Option`. + fn shortcut_static_class( + is_declaration: bool, + class_symbol_id: Option, + object: &Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Option<(SymbolId, ReferenceId)> { + if is_declaration + && let Some(class_symbol_id) = class_symbol_id + && let Expression::Identifier(ident) = object + { + let reference_id = ident.reference_id(); + if let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() + && symbol_id == class_symbol_id + { + return Some((class_symbol_id, reference_id)); + } + } + + None + } + + /// Transform call expression where callee is private field. + /// + /// Not loose: + /// * Instance prop: `object.#prop(arg)` -> `_classPrivateFieldGet2(_prop, object).call(object, arg)` + /// * Static prop: `object.#prop(arg)` -> `_assertClassBrand(Class, object, _prop)._.call(object, arg)` + /// * Instance method: `object.#method(arg)` -> `_assertClassBrand(_Class_brand, object, _prop).call(object, arg)` + /// * Static method: `object.#method(arg)` -> `_assertClassBrand(Class, object, _prop).call(object, arg)` + /// + /// Loose: `object.#prop(arg)` -> `_classPrivateFieldLooseBase(object, _prop)[_prop](arg)` + /// + /// Output in all cases contains a `CallExpression`, so mutate existing `CallExpression` + /// rather than creating a new one. + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::CallExpression` + #[inline] + pub(super) fn transform_call_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::CallExpression(call_expr) = expr else { unreachable!() }; + if matches!(&call_expr.callee, Expression::PrivateFieldExpression(_)) { + self.transform_call_expression_impl(call_expr, ctx); + } + } + + fn transform_call_expression_impl( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Unfortunately no way to make compiler see that this branch is provably unreachable. + // This function is much too large to inline. + let Expression::PrivateFieldExpression(field_expr) = &mut call_expr.callee else { + unreachable!() + }; + + if self.private_fields_as_properties { + // `object.#prop(arg)` -> `_classPrivateFieldLooseBase(object, _prop)[_prop](arg)` + let prop_binding = self.classes_stack.find_private_prop(&field_expr.field).prop_binding; + + let object = field_expr.object.take_in(ctx.ast); + call_expr.callee = Expression::from(Self::create_private_field_member_expr_loose( + object, + prop_binding, + field_expr.span, + self.ctx, + ctx, + )); + return; + } + + let (callee, object) = self.transform_private_field_callee(field_expr, ctx); + Self::substitute_callee_and_insert_context(call_expr, callee, object, ctx); + } + + /// Substitute callee and add object as first argument to call expression. + /// + /// Non-Optional: + /// * `callee(...arguments)` -> `callee.call(object, ...arguments)` + /// + /// Optional: + /// * `callee?.(...arguments)` -> `callee?.call(object, ...arguments)` + fn substitute_callee_and_insert_context( + call_expr: &mut CallExpression<'a>, + callee: Expression<'a>, + context: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) { + // Substitute `.call` as callee of call expression + call_expr.callee = Expression::from(ctx.ast.member_expression_static( + SPAN, + callee, + ctx.ast.identifier_name(SPAN, Atom::from("call")), + // Make sure the `callee` can access `call` safely. i.e `callee?.()` -> `callee?.call()` + mem::replace(&mut call_expr.optional, false), + )); + // Insert `context` to call arguments + call_expr.arguments.insert(0, Argument::from(context)); + } + + /// Transform [`CallExpression::callee`] or [`TaggedTemplateExpression::tag`] that is a private field. + /// + /// Returns two expressions for `callee` and `object`: + /// + /// Instance prop: + /// * `this.#prop` -> + /// callee: `_classPrivateFieldGet(_prop, this)` + /// object: `this` + /// * `this.obj.#prop` -> + /// callee: `_classPrivateFieldGet(_prop, _this$obj = this.obj);` + /// object: `_this$obj` + /// + /// Static prop: + /// * `this.#prop` -> + /// callee: `_assertClassBrand(Class, this, _prop)._` + /// object: `this` + /// * `this.obj.#prop` -> + /// callee: `_assertClassBrand(Class, (_this$obj = this.obj), _prop)._` + /// object: `_this$obj` + fn transform_private_field_callee( + &mut self, + field_expr: &mut PrivateFieldExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, Expression<'a>) { + let span = field_expr.span; + // `(object.#method)()` + // ^^^^^^^^^^^^^^^^ is a parenthesized expression + let object = field_expr.object.get_inner_expression_mut().take_in(ctx.ast); + + let Some(ResolvedPrivateProp { + prop_binding, + class_bindings, + is_static, + is_method, + is_accessor, + is_declaration, + }) = self.classes_stack.find_readable_private_prop(&field_expr.field) + else { + let (object1, object2) = self.duplicate_object(object, ctx); + return ( + self.create_sequence_with_write_only_error( + &field_expr.field.name, + object1, + span, + ctx, + ), + object2, + ); + }; + + let prop_ident = prop_binding.create_read_expression(ctx); + + // Get replacement for callee + + if is_static { + // `object.#prop(arg)` -> `_assertClassBrand(Class, object, _prop)._.call(object, arg)` + // or shortcut `_prop._.call(object, arg)` + + // TODO: Ensure there are tests for nested classes with references to private static props + // of outer class inside inner class, to make sure we're getting the right `class_bindings`. + + // If `object` is reference to class name, there's no need for the class brand assertion + // TODO: Combine this check with `duplicate_object`. Both check if `object` is an identifier, + // and look up the `SymbolId` + if Self::shortcut_static_class( + is_declaration, + class_bindings.name_symbol_id(), + &object, + ctx, + ) + .is_some() + { + if is_method { + // (`_prop`, object) + (prop_ident, object) + } else if is_accessor { + // `(_prop.call(object), object)` + let (object1, object2) = self.duplicate_object(object, ctx); + let callee = create_call_call(prop_ident, object1, span, ctx); + (callee, object2) + } else { + // (`_prop._`, object) + (Self::create_underscore_member_expression(prop_ident, span, ctx), object) + } + } else { + let class_binding = class_bindings.get_or_init_static_binding(ctx); + let class_ident = class_binding.create_read_expression(ctx); + + // Make 2 copies of `object` + let (object1, object2) = self.duplicate_object(object, ctx); + + let assert_obj = if is_method { + if is_accessor { + // `_prop.call(_assertClassBrand(Class, object))` + let object = + self.create_assert_class_brand_without_value(class_ident, object1, ctx); + create_call_call(prop_ident, object, span, ctx) + } else { + // `_assertClassBrand(Class, object, _prop)` + self.create_assert_class_brand(class_ident, object1, prop_ident, span, ctx) + } + } else { + // `_assertClassBrand(Class, object, _prop)._` + self.create_assert_class_brand_underscore( + class_ident, + object1, + prop_ident, + span, + ctx, + ) + }; + + (assert_obj, object2) + } + } else if is_method { + let brand_binding = class_bindings.brand(); + let brand_ident = brand_binding.create_read_expression(ctx); + // `object.#method(arg)` -> `_assetClassBrand(_Class_brand, _prop, object).call(object, arg)` + // Make 2 copies of `object` + let (object1, object2) = self.duplicate_object(object, ctx); + + // `(_Class_brand, this)` + let callee = if is_accessor { + // `_prop.call(_assertClassBrand(_Class_brand, object))` + let object = + self.create_assert_class_brand_without_value(brand_ident, object1, ctx); + create_call_call(prop_ident, object, span, ctx) + } else { + // `_assertClassBrand(_Class_brand, object, _prop)` + self.create_assert_class_brand(brand_ident, object1, prop_ident, span, ctx) + }; + (callee, object2) + } else { + // `object.#prop(arg)` -> `_classPrivateFieldGet2(_prop, object).call(object, arg)` + // Make 2 copies of `object` + let (object1, object2) = self.duplicate_object(object, ctx); + + // `_classPrivateFieldGet2(_prop, object)` + (self.create_private_field_get(prop_ident, object1, span, ctx), object2) + } + } + + /// Transform assignment to private field. + /// + /// Not loose: + /// * Instance: See [`ClassProperties::transform_instance_assignment_expression`]. + /// * Static: See [`ClassProperties::transform_static_assignment_expression`]. + /// + /// Loose: `object.#prop = value` -> `_classPrivateFieldLooseBase(object, _prop)[_prop] = value`. + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::AssignmentExpression` + #[inline] + pub(super) fn transform_assignment_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + if matches!(&assign_expr.left, AssignmentTarget::PrivateFieldExpression(_)) { + self.transform_assignment_expression_impl(expr, ctx); + } + } + + fn transform_assignment_expression_impl( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Unfortunately no way to make compiler see that these branches are provably unreachable. + // This function is much too large to inline, because `transform_static_assignment_expression` + // and `transform_instance_assignment_expression` are inlined into it. + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + let AssignmentTarget::PrivateFieldExpression(field_expr) = &mut assign_expr.left else { + unreachable!() + }; + + let ResolvedGetSetPrivateProp { + get_binding, + set_binding, + class_bindings, + is_static, + is_declaration, + is_method, + .. + } = self.classes_stack.find_get_set_private_prop(&field_expr.field); + + if self.private_fields_as_properties { + // `object.#prop = value` -> `_classPrivateFieldLooseBase(object, _prop)[_prop] = value` + // Same for all other assignment operators e.g. `+=`, `&&=`, `??=`. + let object = field_expr.object.take_in(ctx.ast); + let replacement = Self::create_private_field_member_expr_loose( + object, + // At least one of `get_binding` or `set_binding` is always present + get_binding.unwrap_or_else(|| set_binding.as_ref().unwrap()), + field_expr.span, + self.ctx, + ctx, + ); + assign_expr.left = AssignmentTarget::from(replacement); + return; + } + + // Note: `transform_static_assignment_expression` and `transform_instance_assignment_expression` + // are marked `#[inline]`, so hopefully compiler will see that clones of `BoundIdentifier`s + // can be elided. + // Can't break this up into separate functions otherwise, as `&BoundIdentifier`s keep `&self` ref + // taken by `lookup_private_property` alive. + // TODO: Try to find a way around this. + if is_static && !is_method { + // TODO: No temp var is required if able to use shortcut version, so want to skip calling + // `class_bindings.get_or_init_temp_binding(ctx)` if shortcut can be used. + // But can't pass `class_bindings` as a `&mut ClassBinding` into + // `transform_static_assignment_expression` due to borrow-checker restrictions. + // If clone it, then any update to `temp` field is not stored globally, so that doesn't work. + // Solution will have to be to break up `transform_static_assignment_expression` into 2 methods + // for shortcut/no shortcut and do the "can we shortcut?" check here. + // Then only create temp var for the "no shortcut" branch, and clone the resulting binding + // before passing it to the "no shortcut" method. What a palaver! + let class_binding = class_bindings.get_or_init_static_binding(ctx); + let class_binding = class_binding.clone(); + let class_symbol_id = class_bindings.name_symbol_id(); + // Unwrap is safe because `is_method` is false, then static private prop is always have a `get_binding` + // and `set_binding` and they are always are the same. + let prop_binding = get_binding.cloned().unwrap(); + + self.transform_static_assignment_expression( + expr, + &prop_binding, + &class_binding, + class_symbol_id, + is_declaration, + ctx, + ); + } else if !is_method || !assign_expr.operator.is_assign() || set_binding.is_none() { + let class_binding = is_method.then(|| { + if is_static { + class_bindings.get_or_init_static_binding(ctx).clone() + } else { + class_bindings.brand().clone() + } + }); + let get_binding = get_binding.cloned(); + let set_binding = set_binding.cloned(); + + self.transform_instance_assignment_expression( + expr, + get_binding.as_ref(), + set_binding.as_ref(), + class_binding.as_ref(), + ctx, + ); + } else { + // `object.#setter = object.#setter2 = value` + // Leave this to `transform_assignment_target` to handle + // TODO: After we have alternative to `classPrivateSetter` helper, + // we can handle this here. + } + } + + /// Transform assignment expression with static private prop as assignee. + /// + /// * `object.#prop = value` + /// -> `_prop._ = _assertClassBrand(Class, object, value)` + /// * `object.#prop += value` + /// -> `_prop._ = _assertClassBrand(Class, object, _assertClassBrand(Class, object, _prop)._ + value)` + /// * `object.#prop &&= value` + /// -> `_assertClassBrand(Class, object, _prop)._ && (_prop._ = _assertClassBrand(Class, object, value))` + /// + /// Output in some cases contains an `AssignmentExpression`, so mutate existing `AssignmentExpression` + /// rather than creating a new one when possible. + // + // `#[inline]` so that compiler sees `expr` is an `Expression::AssignmentExpression` with + // `AssignmentTarget::PrivateFieldExpression` on left, and that clones in + // `transform_assignment_expression` can be elided. + #[inline] + fn transform_static_assignment_expression( + &self, + expr: &mut Expression<'a>, + prop_binding: &BoundIdentifier<'a>, + class_binding: &BoundIdentifier<'a>, + class_symbol_id: Option, + is_declaration: bool, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + let operator = assign_expr.operator; + let AssignmentTarget::PrivateFieldExpression(field_expr) = &mut assign_expr.left else { + unreachable!() + }; + + // Check if object (`object` in `object.#prop`) is a reference to class name + // TODO: Combine this check with `duplicate_object`. Both check if `object` is an identifier, + // and look up the `SymbolId`. + let object_reference = + Self::shortcut_static_class(is_declaration, class_symbol_id, &field_expr.object, ctx); + + // If `object` is reference to class name, there's no need for the class brand assertion. + // `Class.#prop = value` -> `_prop._ = value` + // `Class.#prop += value` -> `_prop._ = _prop._ + value` + // `Class.#prop &&= value` -> `_prop._ && (_prop._ = 1)` + // TODO(improve-on-babel): These shortcuts could be shorter - just swap `Class.#prop` for `_prop._`. + // Or does that behave slightly differently if `Class.#prop` is an object with `valueOf` method? + if let Some((class_symbol_id, object_reference_id)) = object_reference { + // Replace left side of assignment with `_prop._` + let field_expr_span = field_expr.span; + assign_expr.left = Self::create_underscore_member_expr_target( + prop_binding.create_read_expression(ctx), + field_expr_span, + ctx, + ); + + // Delete reference for `object` as `object.#prop` has been removed + ctx.scoping_mut().delete_resolved_reference(class_symbol_id, object_reference_id); + + if operator == AssignmentOperator::Assign { + // `Class.#prop = value` -> `_prop._ = value` + // Left side already replaced with `_prop._`. Nothing further to do. + } else { + let prop_obj = Self::create_underscore_member_expression( + prop_binding.create_read_expression(ctx), + field_expr_span, + ctx, + ); + + if let Some(operator) = operator.to_binary_operator() { + // `Class.#prop += value` -> `_prop._ = _prop._ + value` + let value = assign_expr.right.take_in(ctx.ast); + assign_expr.operator = AssignmentOperator::Assign; + assign_expr.right = ctx.ast.expression_binary(SPAN, prop_obj, operator, value); + } else if let Some(operator) = operator.to_logical_operator() { + // `Class.#prop &&= value` -> `_prop._ && (_prop._ = value)` + let span = assign_expr.span; + assign_expr.span = SPAN; + assign_expr.operator = AssignmentOperator::Assign; + let right = expr.take_in(ctx.ast); + *expr = ctx.ast.expression_logical(span, prop_obj, operator, right); + } else { + // The above covers all types of `AssignmentOperator` + unreachable!(); + } + } + } else { + // Substitute left side of assignment with `_prop._`, and get owned `object` from old left side + let assignee = Self::create_underscore_member_expr_target( + prop_binding.create_read_expression(ctx), + SPAN, + ctx, + ); + let old_assignee = mem::replace(&mut assign_expr.left, assignee); + let field_expr = match old_assignee { + AssignmentTarget::PrivateFieldExpression(field_expr) => field_expr.unbox(), + _ => unreachable!(), + }; + let object = field_expr.object.into_inner_expression(); + + let class_ident = class_binding.create_read_expression(ctx); + let value = assign_expr.right.take_in(ctx.ast); + + if operator == AssignmentOperator::Assign { + // Replace right side of assignment with `_assertClassBrand(Class, object, _prop)` + // TODO: Ensure there are tests for nested classes with references to private static props + // of outer class inside inner class, to make sure we're getting the right `class_binding`. + assign_expr.right = + self.create_assert_class_brand(class_ident, object, value, SPAN, ctx); + } else { + let class_ident = class_binding.create_read_expression(ctx); + let value = assign_expr.right.take_in(ctx.ast); + + // Make 2 copies of `object` + let (object1, object2) = self.duplicate_object(object, ctx); + + let prop_ident = prop_binding.create_read_expression(ctx); + let class_ident2 = class_binding.create_read_expression(ctx); + + if let Some(operator) = operator.to_binary_operator() { + // `object.#prop += value` + // -> `_prop._ = _assertClassBrand(Class, object, _assertClassBrand(Class, object, _prop)._ + value)` + + // TODO(improve-on-babel): Are 2 x `_assertClassBrand` calls required? + // Wouldn't `_prop._ = _assertClassBrand(Class, object, _prop)._ + value` do the same? + + // `_assertClassBrand(Class, object, _prop)._` + let get_expr = self.create_assert_class_brand_underscore( + class_ident, + object2, + prop_ident, + SPAN, + ctx, + ); + // `_assertClassBrand(Class, object, _prop)._ + value` + let value = ctx.ast.expression_binary(SPAN, get_expr, operator, value); + // `_assertClassBrand(Class, object, _assertClassBrand(Class, object, _prop)._ + value)` + assign_expr.right = + self.create_assert_class_brand(class_ident2, object1, value, SPAN, ctx); + } else if let Some(operator) = operator.to_logical_operator() { + // `object.#prop &&= value` + // -> `_assertClassBrand(Class, object, _prop)._ && (_prop._ = _assertClassBrand(Class, object, value))` + + // TODO(improve-on-babel): Are 2 x `_assertClassBrand` calls required? + // Wouldn't `_assertClassBrand(Class, object, _prop)._ && _prop._ = value` do the same? + + // `_assertClassBrand(Class, object, _prop)._` + let left = self.create_assert_class_brand_underscore( + class_ident, + object1, + prop_ident, + SPAN, + ctx, + ); + // Mutate existing assignment expression to `_prop._ = _assertClassBrand(Class, object, value)` + // and take ownership of it + let span = assign_expr.span; + assign_expr.span = SPAN; + assign_expr.operator = AssignmentOperator::Assign; + assign_expr.right = + self.create_assert_class_brand(class_ident2, object2, value, SPAN, ctx); + let right = expr.take_in(ctx.ast); + // `_assertClassBrand(Class, object, _prop)._ && (_prop._ = _assertClassBrand(Class, object, value))` + *expr = ctx.ast.expression_logical(span, left, operator, right); + } else { + // The above covers all types of `AssignmentOperator` + unreachable!(); + } + } + } + } + + /// Transform assignment expression with instance private prop as assignee. + /// + /// * `object.#prop = value` + /// -> `_classPrivateFieldSet2(_prop, object, value)` + /// * `object.#prop += value` + /// -> `_classPrivateFieldSet2(_prop, object, _classPrivateFieldGet2(_prop, object) + value)` + /// * `object.#prop &&= value` + /// -> `_classPrivateFieldGet2(_prop, object) && _classPrivateFieldSet2(_prop, object, value)` + // + // `#[inline]` so that compiler sees `expr` is an `Expression::AssignmentExpression` with + // `AssignmentTarget::PrivateFieldExpression` on left, and that clones in + // `transform_assignment_expression` can be elided. + #[inline] + fn transform_instance_assignment_expression( + &self, + expr: &mut Expression<'a>, + get_binding: Option<&BoundIdentifier<'a>>, + set_binding: Option<&BoundIdentifier<'a>>, + class_binding: Option<&BoundIdentifier<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + let assign_expr = match expr.take_in(ctx.ast) { + Expression::AssignmentExpression(assign_expr) => assign_expr.unbox(), + _ => unreachable!(), + }; + let AssignmentExpression { span, operator, right: value, left } = assign_expr; + let AssignmentTarget::PrivateFieldExpression(field_expr) = left else { unreachable!() }; + let PrivateFieldExpression { field, object, .. } = field_expr.unbox(); + + if operator == AssignmentOperator::Assign { + // `object.#prop = value` -> `_classPrivateFieldSet2(_prop, object, value)` + *expr = self.create_private_setter( + &field.name, + class_binding, + set_binding, + object, + value, + span, + ctx, + ); + } else { + // Make 2 copies of `object` + let (object1, object2) = self.duplicate_object(object.into_inner_expression(), ctx); + + if let Some(operator) = operator.to_binary_operator() { + // `object.#prop += value` + // -> `_classPrivateFieldSet2(_prop, object, _classPrivateFieldGet2(_prop, object) + value)` + + // `_classPrivateFieldGet2(_prop, object)` + let get_call = self.create_private_getter( + &field.name, + class_binding, + get_binding, + object2, + SPAN, + ctx, + ); + + // `_classPrivateFieldGet2(_prop, object) + value` + let value = ctx.ast.expression_binary(SPAN, get_call, operator, value); + + // `_classPrivateFieldSet2(_prop, object, _classPrivateFieldGet2(_prop, object) + value)` + *expr = self.create_private_setter( + &field.name, + class_binding, + set_binding, + object1, + value, + span, + ctx, + ); + } else if let Some(operator) = operator.to_logical_operator() { + // `object.#prop &&= value` + // -> `_classPrivateFieldGet2(_prop, object) && _classPrivateFieldSet2(_prop, object, value)` + + // `_classPrivateFieldGet2(_prop, object)` + let get_call = self.create_private_getter( + &field.name, + class_binding, + get_binding, + object1, + SPAN, + ctx, + ); + + // `_classPrivateFieldSet2(_prop, object, value)` + let set_call = self.create_private_setter( + &field.name, + class_binding, + set_binding, + object2, + value, + SPAN, + ctx, + ); + // `_classPrivateFieldGet2(_prop, object) && _classPrivateFieldSet2(_prop, object, value)` + *expr = ctx.ast.expression_logical(span, get_call, operator, set_call); + } else { + // The above covers all types of `AssignmentOperator` + unreachable!(); + } + } + } + + /// Transform update expression (`++` or `--`) where argument is private field. + /// + /// Instance prop (not loose): + /// + /// * `++object.#prop` -> + /// ```js + /// _classPrivateFieldSet( + /// _prop, object, + /// (_object$prop = _classPrivateFieldGet(_prop, object), ++_object$prop) + /// ), + /// ``` + /// + /// * `object.#prop++` -> + /// ```js + /// ( + /// _classPrivateFieldSet( + /// _prop, object, + /// ( + /// _object$prop = _classPrivateFieldGet(_prop, object), + /// _object$prop2 = _object$prop++, + /// _object$prop + /// ) + /// ), + /// _object$prop2 + /// ) + /// ``` + /// + /// Static prop (not loose): + /// + /// * `++object.#prop` -> + /// ```js + /// _prop._ = _assertClassBrand( + /// Class, object, + /// (_object$prop = _assertClassBrand(Class, object, _prop)._, ++_object$prop) + /// ) + /// ``` + /// + /// * `object.#prop++` -> + /// ```js + /// ( + /// _prop._ = _assertClassBrand( + /// Class, object, + /// ( + /// _object$prop = _assertClassBrand(Class, object, _prop)._, + /// _object$prop2 = _object$prop++, + /// _object$prop + /// ) + /// ), + /// _object$prop2 + /// ) + /// ``` + /// + /// Loose: + /// `++object.#prop` -> `++_classPrivateFieldLooseBase(object, _prop)[_prop]` + /// `object.#prop++` -> `_classPrivateFieldLooseBase(object, _prop)[_prop]++` + /// + /// Output in all cases contains an `UpdateExpression`, so mutate existing `UpdateExpression` + /// rather than creating a new one. + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::UpdateExpression` + #[inline] + pub(super) fn transform_update_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::UpdateExpression(update_expr) = expr else { unreachable!() }; + if matches!(&update_expr.argument, SimpleAssignmentTarget::PrivateFieldExpression(_)) { + self.transform_update_expression_impl(expr, ctx); + } + } + + // TODO: Split up this function into 2 halves for static and instance props + fn transform_update_expression_impl( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Unfortunately no way to make compiler see that these branches are provably unreachable. + // This function is much too large to inline. + let Expression::UpdateExpression(update_expr) = expr else { unreachable!() }; + let field_expr = match &mut update_expr.argument { + SimpleAssignmentTarget::PrivateFieldExpression(field_expr) => field_expr.as_mut(), + _ => unreachable!(), + }; + + if self.private_fields_as_properties { + let prop_binding = self.classes_stack.find_private_prop(&field_expr.field).prop_binding; + // `object.#prop++` -> `_classPrivateFieldLooseBase(object, _prop)[_prop]++` + let object = field_expr.object.take_in(ctx.ast); + let replacement = Self::create_private_field_member_expr_loose( + object, + prop_binding, + field_expr.span, + self.ctx, + ctx, + ); + update_expr.argument = SimpleAssignmentTarget::from(replacement); + return; + } + + let ResolvedGetSetPrivateProp { + get_binding, + set_binding, + class_bindings, + is_static, + is_method, + is_declaration, + .. + } = self.classes_stack.find_get_set_private_prop(&field_expr.field); + + let temp_var_name_base = get_var_name_from_node(field_expr); + + // TODO(improve-on-babel): Could avoid `move_expression` here and replace `update_expr.argument` instead. + // Only doing this first to match the order Babel creates temp vars. + let object = field_expr.object.get_inner_expression_mut().take_in(ctx.ast); + + if is_static && !is_method { + // Unwrap is safe because `is_method` is false, then private prop is always have a `get_binding` + // and `set_binding` and they are always are the same. + let prop_binding = get_binding.unwrap(); + let prop_ident = prop_binding.create_read_expression(ctx); + let prop_ident2 = prop_binding.create_read_expression(ctx); + // If `object` is reference to class name, and class is declaration, use shortcuts: + // `++Class.#prop` -> `_prop._ = ((_Class$prop = _prop._), ++_Class$prop)` + // `Class.#prop++` -> `_prop._ = (_Class$prop = _prop._, _Class$prop2 = _Class$prop++, _Class$prop), _Class$prop2` + + // TODO(improve-on-babel): These shortcuts could be shorter - just `_prop._++` / `++_prop._`. + // Or does that behave slightly differently if `Class.#prop` is an object with `valueOf` method? + // TODO(improve-on-babel): No reason not to apply these shortcuts for class expressions too. + + // ``` + // _prop._ = _assertClassBrand( + // Class, object, + // (_object$prop = _assertClassBrand(Class, object, _prop)._, ++_object$prop) + // ) + // ``` + + // TODO(improve-on-babel): Are 2 x `_assertClassBrand` calls required? + // Wouldn't `++_assertClassBrand(C, object, _prop)._` do the same? + + // Check if object (`object` in `object.#prop`) is a reference to class name + // TODO: Combine this check with `duplicate_object`. Both check if `object` is an identifier, + // and look up the `SymbolId`. + let object_reference = Self::shortcut_static_class( + is_declaration, + class_bindings.name_symbol_id(), + &object, + ctx, + ); + + // `_assertClassBrand(Class, object, _prop)._` or `_prop._` + let (get_expr, object, class_ident) = if let Some(object_reference) = object_reference { + // Delete reference for `object` as `object.#prop` is being removed + let (class_symbol_id, object_reference_id) = object_reference; + ctx.scoping_mut().delete_resolved_reference(class_symbol_id, object_reference_id); + + // `_prop._` + let get_expr = Self::create_underscore_member_expression(prop_ident, SPAN, ctx); + (get_expr, object, None) + } else { + let class_binding = class_bindings.get_or_init_static_binding(ctx); + let class_ident = class_binding.create_read_expression(ctx); + let class_ident2 = class_binding.create_read_expression(ctx); + + // Make 2 copies of `object` + let (object1, object2) = self.duplicate_object(object, ctx); + + // `_assertClassBrand(Class, object, _prop)._` + let get_call = self.create_assert_class_brand_underscore( + class_ident, + object2, + prop_ident, + SPAN, + ctx, + ); + (get_call, object1, Some(class_ident2)) + }; + + // `_object$prop = _assertClassBrand(Class, object, _prop)._` + let temp_binding = self.ctx.var_declarations.create_uid_var(&temp_var_name_base, ctx); + let assignment = create_assignment(&temp_binding, get_expr, ctx); + + // `++_object$prop` / `_object$prop++` (reusing existing `UpdateExpression`) + let UpdateExpression { span, prefix, .. } = **update_expr; + update_expr.span = SPAN; + update_expr.argument = temp_binding.create_read_write_simple_target(ctx); + let update_expr = expr.take_in(ctx.ast); + + if prefix { + // Source = `++object.#prop` (prefix `++`) + + // `(_object$prop = _assertClassBrand(Class, object, _prop)._, ++_object$prop)` + let mut value = ctx + .ast + .expression_sequence(SPAN, ctx.ast.vec_from_array([assignment, update_expr])); + + // If no shortcut, wrap in `_assertClassBrand(Class, object, )` + if let Some(class_ident) = class_ident { + value = self.create_assert_class_brand(class_ident, object, value, SPAN, ctx); + } + + // `_prop._ = ` + *expr = ctx.ast.expression_assignment( + span, + AssignmentOperator::Assign, + Self::create_underscore_member_expr_target(prop_ident2, SPAN, ctx), + value, + ); + } else { + // Source = `object.#prop++` (postfix `++`) + + // `_object$prop2 = _object$prop++` + let temp_binding2 = + self.ctx.var_declarations.create_uid_var(&temp_var_name_base, ctx); + let assignment2 = create_assignment(&temp_binding2, update_expr, ctx); + + // `(_object$prop = _assertClassBrand(Class, object, _prop)._, _object$prop2 = _object$prop++, _object$prop)` + let mut value = ctx.ast.expression_sequence( + SPAN, + ctx.ast.vec_from_array([ + assignment, + assignment2, + temp_binding.create_read_expression(ctx), + ]), + ); + + // If no shortcut, wrap in `_assertClassBrand(Class, object, )` + if let Some(class_ident) = class_ident { + value = self.create_assert_class_brand(class_ident, object, value, SPAN, ctx); + } + + // `_prop._ = ` + let assignment3 = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + Self::create_underscore_member_expr_target(prop_ident2, SPAN, ctx), + value, + ); + + // `(_prop._ = , _object$prop2)` + // TODO(improve-on-babel): Final `_object$prop2` is only needed if this expression + // is consumed (i.e. not in an `ExpressionStatement`) + *expr = ctx.ast.expression_sequence( + span, + ctx.ast + .vec_from_array([assignment3, temp_binding2.create_read_expression(ctx)]), + ); + } + } else { + // Clone as borrow restrictions. + // TODO: Try to find a way to avoid this. + let class_binding = is_method.then(|| { + if is_static { + class_bindings.get_or_init_static_binding(ctx).clone() + } else { + class_bindings.brand().clone() + } + }); + let get_binding = get_binding.cloned(); + let set_binding = set_binding.cloned(); + let private_name = field_expr.field.name; + + // Make 2 copies of `object` + let (object1, object2) = self.duplicate_object(object, ctx); + + // `_classPrivateFieldGet(_prop, object)` + let get_call = self.create_private_getter( + &private_name, + class_binding.as_ref(), + get_binding.as_ref(), + object2, + SPAN, + ctx, + ); + + // `_object$prop = _classPrivateFieldGet(_prop, object)` + let temp_binding = self.ctx.var_declarations.create_uid_var(&temp_var_name_base, ctx); + let assignment = create_assignment(&temp_binding, get_call, ctx); + + // `++_object$prop` / `_object$prop++` (reusing existing `UpdateExpression`) + let UpdateExpression { span, prefix, .. } = **update_expr; + update_expr.span = SPAN; + update_expr.argument = temp_binding.create_read_write_simple_target(ctx); + let update_expr = expr.take_in(ctx.ast); + + if prefix { + // Source = `++object.#prop` (prefix `++`) + // `(_object$prop = _classPrivateFieldGet(_prop, object), ++_object$prop)` + let value = ctx + .ast + .expression_sequence(SPAN, ctx.ast.vec_from_array([assignment, update_expr])); + // `_classPrivateFieldSet(_prop, object, )` + *expr = self.create_private_setter( + &private_name, + class_binding.as_ref(), + set_binding.as_ref(), + object1, + value, + span, + ctx, + ); + } else { + // Source = `object.#prop++` (postfix `++`) + // `_object$prop2 = _object$prop++` + let temp_binding2 = + self.ctx.var_declarations.create_uid_var(&temp_var_name_base, ctx); + let assignment2 = create_assignment(&temp_binding2, update_expr, ctx); + + // `(_object$prop = _classPrivateFieldGet(_prop, object), _object$prop2 = _object$prop++, _object$prop)` + let value = ctx.ast.expression_sequence( + SPAN, + ctx.ast.vec_from_array([ + assignment, + assignment2, + temp_binding.create_read_expression(ctx), + ]), + ); + + // `_classPrivateFieldSet(_prop, object, )` + let set_call = self.create_private_setter( + &private_name, + class_binding.as_ref(), + set_binding.as_ref(), + object1, + value, + span, + ctx, + ); + // `(_classPrivateFieldSet(_prop, object, ), _object$prop2)` + // TODO(improve-on-babel): Final `_object$prop2` is only needed if this expression + // is consumed (i.e. not in an `ExpressionStatement`) + *expr = ctx.ast.expression_sequence( + span, + ctx.ast.vec_from_array([set_call, temp_binding2.create_read_expression(ctx)]), + ); + } + } + } + + /// Transform chain expression where includes a private field. + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::ChainExpression` + #[inline] + pub(super) fn transform_chain_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some((result, chain_expr)) = self.transform_chain_expression_impl(expr, ctx) { + *expr = Self::wrap_conditional_check(result, chain_expr, ctx); + } + } + + fn transform_chain_expression_impl( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option<(Expression<'a>, Expression<'a>)> { + let Expression::ChainExpression(chain_expr) = expr else { unreachable!() }; + + let element = &mut chain_expr.expression; + if matches!(element, ChainElement::PrivateFieldExpression(_)) { + // The PrivateFieldExpression must be transformed, so we can convert it to a normal expression here. + let mut chain_expr = Self::convert_chain_expression_to_expression(expr, ctx); + let result = self + .transform_private_field_expression_of_chain_expression(&mut chain_expr, ctx) + .expect("The ChainExpression must contain at least one optional expression, so it can never be `None` here."); + Some((result, chain_expr)) + } else if let Some(result) = self.transform_chain_expression_element(element, ctx) { + let chain_expr = Self::convert_chain_expression_to_expression(expr, ctx); + Some((result, chain_expr)) + } else { + // "Entering this branch indicates that the chain element has been changed and updated directly in + // `element` or do nothing because haven't found any private field." + None + } + } + + /// Transform non-private field expression of chain element. + /// + /// [`ChainElement::PrivateFieldExpression`] is handled in [`Self::transform_chain_expression`]. + fn transform_chain_expression_element( + &mut self, + element: &mut ChainElement<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + match element { + expression @ match_member_expression!(ChainElement) => self + .transform_member_expression_of_chain_expression( + expression.to_member_expression_mut(), + ctx, + ), + ChainElement::CallExpression(call) => { + self.transform_call_expression_of_chain_expression(call, ctx) + } + ChainElement::TSNonNullExpression(non_null) => { + self.transform_chain_element_recursively(&mut non_null.expression, ctx) + } + } + } + + /// Recursively find the first private field expression in the chain element and transform it. + fn transform_chain_element_recursively( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + match expr { + Expression::PrivateFieldExpression(_) => { + self.transform_private_field_expression_of_chain_expression(expr, ctx) + } + match_member_expression!(Expression) => self + .transform_member_expression_of_chain_expression( + expr.to_member_expression_mut(), + ctx, + ), + Expression::CallExpression(call) => { + self.transform_call_expression_of_chain_expression(call, ctx) + } + _ => { + debug_assert_expr_is_not_parenthesis_or_typescript_syntax( + expr, + &self.ctx.source_path, + ); + None + } + } + } + + /// Go through the part of chain element and transform the object/callee of first encountered optional member/call. + /// + /// Ident: + /// * `Foo?.bar`: + /// - Passed-in `expr` will be mutated to `Foo.bar` + /// - Returns `Foo === null || Foo === void 0 ? void 0` + /// + /// MemberExpression: + /// * `Foo?.bar?.baz`: + /// - Passed-in `expr` will be mutated to `_Foo$bar.baz` + /// - Returns `Foo === null || Foo === void 0 ? void 0` + /// + /// CallExpression: + /// See [`Self::transform_call_expression_to_bind_proper_context`] + /// + fn transform_first_optional_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let object = match expr { + Expression::CallExpression(call) => { + if call.optional { + call.optional = false; + if call.callee.is_member_expression() { + // Special case for call expression because we need to make sure it has a proper context + return Some( + self.transform_call_expression_to_bind_proper_context(expr, ctx), + ); + } + &mut call.callee + } else { + return self.transform_first_optional_expression(&mut call.callee, ctx); + } + } + Expression::StaticMemberExpression(member) => { + if member.optional { + member.optional = false; + &mut member.object + } else { + return self.transform_first_optional_expression(&mut member.object, ctx); + } + } + Expression::ComputedMemberExpression(member) => { + if member.optional { + member.optional = false; + &mut member.object + } else { + return self.transform_first_optional_expression(&mut member.object, ctx); + } + } + Expression::PrivateFieldExpression(member) => { + if member.optional { + member.optional = false; + &mut member.object + } else { + return self.transform_first_optional_expression(&mut member.object, ctx); + } + } + _ => return None, + }; + + let result = self.transform_expression_to_wrap_nullish_check(object, ctx); + Some(result) + } + + /// Transform private field expression of chain expression. + /// + /// Returns `None` if the `expr` doesn't contain any optional expression. + fn transform_private_field_expression_of_chain_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let Expression::PrivateFieldExpression(field_expr) = expr else { unreachable!() }; + + let is_optional = field_expr.optional; + let object = &mut field_expr.object; + + let result = if is_optional { + Some(self.transform_expression_to_wrap_nullish_check(object, ctx)) + } else { + self.transform_first_optional_expression(object, ctx) + }; + + if matches!(ctx.ancestor(1), Ancestor::CallExpressionCallee(_)) { + // `(Foo?.#m)();` -> `(Foo === null || Foo === void 0 ? void 0 : _m._.bind(Foo))();` + // ^^^^^^^^^^^^ is a call expression, we need to bind the proper context + *expr = self.transform_bindable_private_field(field_expr, ctx); + } else { + self.transform_private_field_expression(expr, ctx); + } + + result + } + + fn transform_member_expression_of_chain_expression( + &mut self, + member: &mut MemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let is_optional = member.optional(); + let object = member.object_mut().get_inner_expression_mut(); + let result = self.transform_chain_element_recursively(object, ctx)?; + if is_optional && !object.is_identifier_reference() { + // `o?.Foo.#self.self?.self.unicorn;` -> `(result ? void 0 : object)?.self.unicorn` + // ^^^^^^^^^^^^^^^^^ the object has transformed, if the current member is optional, + // then we need to wrap it to a conditional expression + let owned_object = object.take_in(ctx.ast); + *object = Self::wrap_conditional_check(result, owned_object, ctx); + None + } else { + Some(result) + } + } + + fn transform_call_expression_of_chain_expression( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let is_optional = call_expr.optional; + + let callee = call_expr.callee.get_inner_expression_mut(); + if matches!(callee, Expression::PrivateFieldExpression(_)) { + let result = self.transform_first_optional_expression(callee, ctx); + // If the `callee` has no optional expression, we need to transform it using `transform_call_expression_impl` directly. + // `Foo.bar.#m?.();` -> `_assertClassBrand(Foo, _Foo$bar = Foo.bar, _m)._?.call(_Foo$bar);` + // ^^^^ only the private field is optional + // Move out parenthesis and typescript syntax + call_expr.callee = callee.take_in(ctx.ast); + self.transform_call_expression_impl(call_expr, ctx); + return result; + } + + let result = self.transform_chain_element_recursively(callee, ctx); + if !is_optional { + return result; + } + + // `o?.Foo.#self.getSelf?.()?.self.#m();` + // ^^^^^^^^^^^ this is a optional function call, to make sure it has a proper context, + // we also need to assign `o?.Foo.#self` to a temp variable, and + // then use it as a first argument of `getSelf` call. + // + // TODO(improve-on-babel): Consider remove this logic, because it seems no runtime behavior change. + let result = result?; + let object = callee.to_member_expression_mut().object_mut(); + let (assignment, context) = self.duplicate_object(object.take_in(ctx.ast), ctx); + *object = assignment; + let callee = call_expr.callee.take_in(ctx.ast); + let callee = Self::wrap_conditional_check(result, callee, ctx); + Self::substitute_callee_and_insert_context(call_expr, callee, context, ctx); + + None + } + + /// Transform expression to wrap nullish check. + /// + /// Returns: + /// * Bound Identifier: `A` -> `A === null || A === void 0` + /// * `this`: `this` -> `this === null || this === void 0` + /// * Unbound Identifier or anything else: `A.B` -> `(_A$B = A.B) === null || _A$B === void 0` + /// + /// NOTE: This method will mutate the passed-in `object` to a second copy of + /// [`Self::duplicate_object_twice`]'s return. + fn transform_expression_to_wrap_nullish_check( + &mut self, + object: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let mut owned_object = object.get_inner_expression_mut().take_in(ctx.ast); + + let owned_object = if let Some(result) = + self.transform_chain_element_recursively(&mut owned_object, ctx) + { + // If the `object` contains PrivateFieldExpression, we need to transform it first. + Self::wrap_conditional_check(result, owned_object, ctx) + } else { + Self::ensure_optional_expression_wrapped_by_chain_expression(owned_object, ctx) + }; + + let (assignment, reference1, reference2) = self.duplicate_object_twice(owned_object, ctx); + *object = reference1; + self.wrap_nullish_check(assignment, reference2, ctx) + } + + /// Converts chain expression to expression + /// + /// - [ChainElement::CallExpression] -> [Expression::CallExpression] + /// - [ChainElement::StaticMemberExpression] -> [Expression::StaticMemberExpression] + /// - [ChainElement::ComputedMemberExpression] -> [Expression::ComputedMemberExpression] + /// - [ChainElement::PrivateFieldExpression] -> [Expression::PrivateFieldExpression] + /// - [ChainElement::TSNonNullExpression] -> [TSNonNullExpression::expression] + // + // `#[inline]` so that compiler sees that `expr` is an [`Expression::ChainExpression`]. + #[inline] + fn convert_chain_expression_to_expression( + expr: &mut Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let Expression::ChainExpression(chain_expr) = expr.take_in(ctx.ast) else { unreachable!() }; + match chain_expr.unbox().expression { + element @ match_member_expression!(ChainElement) => { + Expression::from(element.into_member_expression()) + } + ChainElement::CallExpression(call) => Expression::CallExpression(call), + ChainElement::TSNonNullExpression(non_null) => non_null.unbox().expression, + } + } + + /// Ensure that the expression is wrapped by a chain expression. + /// + /// If the given expression contains optional expression, it will be wrapped by + /// a chain expression, this way we can ensure the remain optional expression can + /// be handled by optional-chaining plugin correctly. + fn ensure_optional_expression_wrapped_by_chain_expression( + expr: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + if Self::has_optional_expression(&expr) { + let chain_element = match expr { + Expression::CallExpression(call) => ChainElement::CallExpression(call), + expr @ match_member_expression!(Expression) => { + ChainElement::from(expr.into_member_expression()) + } + _ => unreachable!(), + }; + ctx.ast.expression_chain(SPAN, chain_element) + } else { + expr + } + } + + /// Recursively check if the expression has optional expression. + #[inline] + fn has_optional_expression(expr: &Expression<'a>) -> bool { + let mut expr = expr; + loop { + match expr { + Expression::CallExpression(call) => { + if call.optional { + return true; + } + expr = call.callee.get_inner_expression(); + } + Expression::StaticMemberExpression(member) => { + if member.optional { + return true; + } + expr = &member.object; + } + Expression::ComputedMemberExpression(member) => { + if member.optional { + return true; + } + expr = &member.object; + } + Expression::PrivateFieldExpression(member) => { + if member.optional { + return true; + } + expr = &member.object; + } + _ => return false, + } + } + } + + /// Transform call expression to bind a proper context. + /// + /// * Callee without a private field: + /// `Foo?.bar()?.zoo?.().#x;` + /// -> + /// `(_Foo$bar$zoo = (_Foo$bar = Foo?.bar())?.zoo) === null || _Foo$bar$zoo === void 0 ? void 0 + /// : babelHelpers.assertClassBrand(Foo, _Foo$bar$zoo.call(_Foo$bar), _x)._;` + /// + /// * Callee has a private field: + /// `o?.Foo.#self.getSelf?.().#m?.();` + /// -> + /// `(_ref = o === null || o === void 0 ? void 0 : (_babelHelpers$assertC = + /// babelHelpers.assertClassBrand(Foo, o.Foo, _self)._).getSelf) === null || + /// _ref === void 0 ? void 0 : babelHelpers.assertClassBrand(Foo, _ref$call + /// = _ref.call(_babelHelpers$assertC), _m)._?.call(_ref$call);` + fn transform_call_expression_to_bind_proper_context( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let Expression::CallExpression(call) = expr else { unreachable!() }; + + // `Foo?.bar()?.zoo?.()` + // ^^^^^^^^^^^ object + // ^^^^^^^^^^^^^^^^^ callee is a member expression + let callee = &mut call.callee; + let callee_member = callee.to_member_expression_mut(); + let is_optional_callee = callee_member.optional(); + let object = callee_member.object_mut().get_inner_expression_mut(); + + let context = if let Some(result) = self.transform_chain_element_recursively(object, ctx) { + if is_optional_callee { + // `o?.Foo.#self?.getSelf?.().#x;` -> `(_ref$getSelf = (_ref2 = _ref = o === null || o === void 0 ? + // ^^ is optional void 0 : babelHelpers.assertClassBrand(Foo, o.Foo, _self)._)` + *object = Self::wrap_conditional_check(result, object.take_in(ctx.ast), ctx); + let (assignment, context) = self.duplicate_object(object.take_in(ctx.ast), ctx); + *object = assignment; + context + } else { + // `o?.Foo.#self.getSelf?.().#m?.();` -> `(_ref = o === null || o === void 0 ? void 0 : (_babelHelpers$assertC = + // babelHelpers.assertClassBrand(Foo, o.Foo, _self)._).getSelf)` + // ^^^^^^^^^^^^^^^^^^^^^^ to make sure get `getSelf` call has a proper context, we need to assign + // the parent of callee (i.e `o?.Foo.#self`) to a temp variable, + // and then use it as a first argument of `_ref.call`. + let (assignment, context) = self.duplicate_object(object.take_in(ctx.ast), ctx); + *object = assignment; + *callee = Self::wrap_conditional_check(result, callee.take_in(ctx.ast), ctx); + context + } + } else { + // `Foo?.bar()?.zoo?.().#x;` -> `(_Foo$bar$zoo = (_Foo$bar = Foo?.bar())?.zoo)` + // ^^^^^^^^^^^^^^^^ this is a optional function call, to make sure it has a proper context, + // we also need to assign `Foo?.bar()` to a temp variable, and then use + // it as a first argument of `_Foo$bar$zoo`. + let (assignment, context) = self.duplicate_object(object.take_in(ctx.ast), ctx); + *object = assignment; + context + }; + + // After the below transformation, the `callee` will be a temp variable. + let result = self.transform_expression_to_wrap_nullish_check(callee, ctx); + let owned_callee = callee.take_in(ctx.ast); + Self::substitute_callee_and_insert_context(call, owned_callee, context, ctx); + result + } + + /// Returns `left === null` + fn wrap_null_check(&self, left: Expression<'a>, ctx: &TraverseCtx<'a>) -> Expression<'a> { + let operator = if self.ctx.assumptions.no_document_all { + BinaryOperator::Equality + } else { + BinaryOperator::StrictEquality + }; + ctx.ast.expression_binary(SPAN, left, operator, ctx.ast.expression_null_literal(SPAN)) + } + + /// Returns `left === void 0` + fn wrap_void0_check(left: Expression<'a>, ctx: &TraverseCtx<'a>) -> Expression<'a> { + let operator = BinaryOperator::StrictEquality; + ctx.ast.expression_binary(SPAN, left, operator, ctx.ast.void_0(SPAN)) + } + + /// Returns `left1 === null || left2 === void 0` + fn wrap_nullish_check( + &self, + left1: Expression<'a>, + left2: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let null_check = self.wrap_null_check(left1, ctx); + if self.ctx.assumptions.no_document_all { + null_check + } else { + let void0_check = Self::wrap_void0_check(left2, ctx); + ctx.ast.expression_logical(SPAN, null_check, LogicalOperator::Or, void0_check) + } + } + + /// Returns `test ? void 0 : alternative` + fn wrap_conditional_check( + test: Expression<'a>, + alternative: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + ctx.ast.expression_conditional(SPAN, test, ctx.ast.void_0(SPAN), alternative) + } + + /// Transform chain expression inside unary expression. + /// + /// Instance prop: + /// * `delete object?.#prop.xyz` + /// -> `object === null || object === void 0 ? true : delete _classPrivateFieldGet(_prop, object).xyz;` + /// * `delete object?.#prop?.xyz;` + /// -> `delete (object === null || object === void 0 ? void 0 : _classPrivateFieldGet(_prop, object))?.xyz;` + /// + /// Static prop: + /// * `delete object?.#prop.xyz` + /// -> `object === null || object === void 0 ? true : delete _assertClassBrand(Foo, object, _prop)._.xyz;` + /// * `delete object?.#prop?.xyz;` + /// -> `delete (object === null || object === void 0 ? void 0 : _assertClassBrand(Foo, object, _prop)._)?.xyz;` + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::UnaryExpression`, + // and make bailing out if is not `delete ` (it rarely will be) a fast path without + // cost of a function call. + #[inline] + pub(super) fn transform_unary_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::UnaryExpression(unary_expr) = expr else { unreachable!() }; + + if unary_expr.operator == UnaryOperator::Delete + && matches!(unary_expr.argument, Expression::ChainExpression(_)) + { + self.transform_unary_expression_impl(expr, ctx); + } + } + + fn transform_unary_expression_impl( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::UnaryExpression(unary_expr) = expr else { unreachable!() }; + debug_assert!(unary_expr.operator == UnaryOperator::Delete); + debug_assert!(matches!(unary_expr.argument, Expression::ChainExpression(_))); + + if let Some((result, chain_expr)) = + self.transform_chain_expression_impl(&mut unary_expr.argument, ctx) + { + *expr = ctx.ast.expression_conditional( + unary_expr.span, + result, + ctx.ast.expression_boolean_literal(SPAN, true), + { + // We still need this unary expr, but it needs to be used as the alternative of the conditional + unary_expr.argument = chain_expr; + expr.take_in(ctx.ast) + }, + ); + } + } + + /// Transform tagged template expression where tag is a private field. + /// + /// Instance prop (not loose): + /// * "object.#prop`xyz`" + /// -> "_classPrivateFieldGet(_prop, object).bind(object)`xyz`" + /// * "object.obj.#prop`xyz`" + /// -> "_classPrivateFieldGet(_prop, _object$obj = object.obj).bind(_object$obj)`xyz`" + /// + /// Static prop (not loose): + /// * "object.#prop`xyz`" + /// -> "_assertClassBrand(Class, object, _prop)._.bind(object)`xyz`" + /// * "object.obj.#prop`xyz`" + /// -> "_assertClassBrand(Class, (_object$obj = object.obj), _prop)._.bind(_object$obj)`xyz`" + /// + /// Loose: + /// ```js + /// object.#prop`xyz` + /// ``` + /// -> + /// ```js + /// _classPrivateFieldLooseBase(object, _prop)[_prop]`xyz` + /// ``` + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::TaggedTemplateExpression` + #[inline] + pub(super) fn transform_tagged_template_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::TaggedTemplateExpression(tagged_temp_expr) = expr else { unreachable!() }; + let Expression::PrivateFieldExpression(field_expr) = &mut tagged_temp_expr.tag else { + return; + }; + + tagged_temp_expr.tag = self.transform_tagged_template_expression_impl(field_expr, ctx); + } + + fn transform_tagged_template_expression_impl( + &mut self, + field_expr: &mut PrivateFieldExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if self.private_fields_as_properties { + // "object.#prop`xyz`" -> "_classPrivateFieldLooseBase(object, _prop)[_prop]`xyz`" + // + // Babel adds an additional `.bind(object)`: + // ```js + // _classPrivateFieldLooseBase(object, _prop)[_prop].bind(object)`xyz`" + // // ^^^^^^^^^^^^^ + // ``` + // But this is not needed, so we omit it. + let prop_binding = self.classes_stack.find_private_prop(&field_expr.field).prop_binding; + + let object = field_expr.object.take_in(ctx.ast); + let replacement = Self::create_private_field_member_expr_loose( + object, + prop_binding, + field_expr.span, + self.ctx, + ctx, + ); + Expression::from(replacement) + } else { + self.transform_bindable_private_field(field_expr, ctx) + } + } + + fn transform_bindable_private_field( + &mut self, + field_expr: &mut PrivateFieldExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let (callee, context) = self.transform_private_field_callee(field_expr, ctx); + + // Return `.bind(object)`, to be substituted as tag of tagged template expression + let callee = Expression::from(ctx.ast.member_expression_static( + SPAN, + callee, + ctx.ast.identifier_name(SPAN, Atom::from("bind")), + false, + )); + let arguments = ctx.ast.vec1(Argument::from(context)); + ctx.ast.expression_call(field_expr.span, callee, NONE, arguments, false) + } + + /// Transform private field in assignment pattern. + /// + /// Instance prop: + /// * `[object.#prop] = arr` -> `[_toSetter(_classPrivateFieldSet, [_prop, object])._] = arr` + /// * `({x: object.#prop} = obj)` -> `({ x: _toSetter(_classPrivateFieldSet, [_prop, object])._ } = obj)` + /// + /// Static prop: + /// (same as `Expression::PrivateFieldExpression` is transformed to) + /// * `[object.#prop] = arr` -> `[_assertClassBrand(Class, object, _prop)._] = arr` + /// * `({x: object.#prop} = obj)` -> `({ x: _assertClassBrand(Class, object, _prop)._ } = obj)` + // + // `#[inline]` because most `AssignmentTarget`s are not `PrivateFieldExpression`s. + // So we want to bail out in that common case without the cost of a function call. + // Transform of `PrivateFieldExpression`s in broken out into `transform_assignment_target_impl` to + // keep this function as small as possible. + #[inline] + pub(super) fn transform_assignment_target( + &mut self, + target: &mut AssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // `object.#prop` in assignment pattern. + // Must be in assignment pattern, as `enter_expression` already transformed `AssignmentExpression`s. + if matches!(target, AssignmentTarget::PrivateFieldExpression(_)) { + self.transform_assignment_target_impl(target, ctx); + } + } + + fn transform_assignment_target_impl( + &mut self, + target: &mut AssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let AssignmentTarget::PrivateFieldExpression(private_field) = target else { + unreachable!() + }; + let replacement = self.transform_private_field_expression_impl(private_field, true, ctx); + *target = AssignmentTarget::from(replacement.into_member_expression()); + } + + /// Transform private field in expression. + /// + /// * Static + /// `#prop in object` -> `_checkInRHS(object) === Class` + /// + /// * Instance prop + /// `#prop in object` -> `_prop.has(_checkInRHS(object))` + /// + /// * Instance method + /// `#method in object` -> `_Class_brand.has(_checkInRHS(object))` + /// + // `#[inline]` so that compiler sees that `expr` is an `Expression::PrivateFieldExpression` + #[inline] + pub(super) fn transform_private_in_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::PrivateInExpression(private_in) = expr.take_in(ctx.ast) else { + unreachable!(); + }; + + *expr = self.transform_private_in_expression_impl(private_in, ctx); + } + + fn transform_private_in_expression_impl( + &mut self, + private_field: ArenaBox<'a, PrivateInExpression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let PrivateInExpression { left, right, span } = private_field.unbox(); + + let ResolvedPrivateProp { class_bindings, prop_binding, is_method, is_static, .. } = + self.classes_stack.find_private_prop(&left); + + if is_static { + let class_binding = class_bindings.get_or_init_static_binding(ctx); + let class_ident = class_binding.create_read_expression(ctx); + let left = self.create_check_in_rhs(right, SPAN, ctx); + return ctx.ast.expression_binary( + span, + left, + BinaryOperator::StrictEquality, + class_ident, + ); + } + + let callee = if is_method { + class_bindings.brand().create_read_expression(ctx) + } else { + prop_binding.create_read_expression(ctx) + }; + let callee = create_member_callee(callee, "has", ctx); + let argument = self.create_check_in_rhs(right, SPAN, ctx); + ctx.ast.expression_call(span, callee, NONE, ctx.ast.vec1(Argument::from(argument)), false) + } + + /// Duplicate object to be used in get/set pair. + /// + /// If `object` may have side effects, create a temp var `_object` and assign to it. + /// + /// * `this` -> `this`, `this` + /// * Bound identifier `object` -> `object`, `object` + /// * Unbound identifier `object` -> `_object = object`, `_object` + /// * Anything else `foo()` -> `_foo = foo()`, `_foo` + /// + /// Returns 2 `Expression`s. The first must be inserted into output first. + pub(super) fn duplicate_object( + &self, + object: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, Expression<'a>) { + debug_assert_expr_is_not_parenthesis_or_typescript_syntax(&object, &self.ctx.source_path); + self.ctx.duplicate_expression(object, false, ctx) + } + + /// Duplicate object to be used in triple. + /// + /// If `object` may have side effects, create a temp var `_object` and assign to it. + /// + /// * `this` -> `this`, `this`, `this` + /// * Bound identifier `object` -> `object`, `object`, `object` + /// * Unbound identifier `object` -> `_object = object`, `_object`, `_object` + /// * Anything else `foo()` -> `_foo = foo()`, `_foo`, `_foo` + /// + /// Returns 3 `Expression`s. The first must be inserted into output first. + fn duplicate_object_twice( + &self, + object: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> (Expression<'a>, Expression<'a>, Expression<'a>) { + debug_assert_expr_is_not_parenthesis_or_typescript_syntax(&object, &self.ctx.source_path); + self.ctx.duplicate_expression_twice(object, false, ctx) + } + + /// `_classPrivateFieldLooseBase(object, _prop)[_prop]`. + /// + /// Takes `&TransformCtx` instead of `&self` to allow passing a `&BoundIdentifier<'a>` returned by + /// `self.private_props_stack.find()`, which takes a partial mut borrow of `self`. + fn create_private_field_member_expr_loose( + object: Expression<'a>, + prop_binding: &BoundIdentifier<'a>, + span: Span, + transform_ctx: &TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> MemberExpression<'a> { + let call_expr = transform_ctx.helper_call_expr( + Helper::ClassPrivateFieldLooseBase, + SPAN, + ctx.ast.vec_from_array([ + Argument::from(object), + Argument::from(prop_binding.create_read_expression(ctx)), + ]), + ctx, + ); + ctx.ast.member_expression_computed( + span, + call_expr, + prop_binding.create_read_expression(ctx), + false, + ) + } + + /// `_classPrivateFieldGet2(_prop, object)` + fn create_private_field_get( + &self, + prop_ident: Expression<'a>, + object: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + self.ctx.helper_call_expr( + Helper::ClassPrivateFieldGet2, + span, + ctx.ast.vec_from_array([Argument::from(prop_ident), Argument::from(object)]), + ctx, + ) + } + + /// `_classPrivateFieldSet2(_prop, object, value)` + fn create_private_field_set( + &self, + prop_ident: Expression<'a>, + object: Expression<'a>, + value: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + self.ctx.helper_call_expr( + Helper::ClassPrivateFieldSet2, + span, + ctx.ast.vec_from_array([ + Argument::from(prop_ident), + Argument::from(object), + Argument::from(value), + ]), + ctx, + ) + } + + /// `_toSetter(_classPrivateFieldSet2, [_prop, object])._` + fn create_to_setter_for_private_field_set( + &self, + prop_ident: Expression<'a>, + object: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let arguments = ctx.ast.expression_array( + SPAN, + ctx.ast.vec_from_array([ + ArrayExpressionElement::from(prop_ident), + ArrayExpressionElement::from(object), + ]), + ); + let arguments = ctx.ast.vec_from_array([ + Argument::from(self.ctx.helper_load(Helper::ClassPrivateFieldSet2, ctx)), + Argument::from(arguments), + ]); + let call = self.ctx.helper_call_expr(Helper::ToSetter, span, arguments, ctx); + Self::create_underscore_member_expression(call, span, ctx) + } + + /// `_toSetter(_prop.bind(object))._` + fn create_to_setter_for_bind_call( + &self, + prop_ident: Expression<'a>, + object: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let prop_call = create_bind_call(prop_ident, object, span, ctx); + let arguments = ctx.ast.vec_from_array([Argument::from(prop_call)]); + let call = self.ctx.helper_call_expr(Helper::ToSetter, span, arguments, ctx); + Self::create_underscore_member_expression(call, span, ctx) + } + + /// `_assertClassBrand(Class, object, value)` or `_assertClassBrand(Class, object, _prop)` + fn create_assert_class_brand( + &self, + class_ident: Expression<'a>, + object: Expression<'a>, + value_or_prop_ident: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + self.ctx.helper_call_expr( + Helper::AssertClassBrand, + span, + ctx.ast.vec_from_array([ + Argument::from(class_ident), + Argument::from(object), + Argument::from(value_or_prop_ident), + ]), + ctx, + ) + } + + /// `_assertClassBrand(Class, object)` + fn create_assert_class_brand_without_value( + &self, + class_ident: Expression<'a>, + object: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let arguments = + ctx.ast.vec_from_array([Argument::from(class_ident), Argument::from(object)]); + self.ctx.helper_call_expr(Helper::AssertClassBrand, SPAN, arguments, ctx) + } + + /// `_assertClassBrand(Class, object, _prop)._` + fn create_assert_class_brand_underscore( + &self, + class_ident: Expression<'a>, + object: Expression<'a>, + prop_ident: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let func_call = self.create_assert_class_brand(class_ident, object, prop_ident, SPAN, ctx); + Self::create_underscore_member_expression(func_call, span, ctx) + } + + /// Create `._` assignment target. + fn create_underscore_member_expr_target( + object: Expression<'a>, + span: Span, + ctx: &TraverseCtx<'a>, + ) -> AssignmentTarget<'a> { + AssignmentTarget::from(Self::create_underscore_member_expr(object, span, ctx)) + } + + /// Create `._` expression. + fn create_underscore_member_expression( + object: Expression<'a>, + span: Span, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + Expression::from(Self::create_underscore_member_expr(object, span, ctx)) + } + + /// Create `._` member expression. + fn create_underscore_member_expr( + object: Expression<'a>, + span: Span, + ctx: &TraverseCtx<'a>, + ) -> MemberExpression<'a> { + ctx.ast.member_expression_static(span, object, create_underscore_ident_name(ctx), false) + } + + /// * Getter: `_prop.call(_assertClassBrand(Class, object))` + /// * Prop: `_privateFieldGet(_prop, object)` + /// * Prop binding is `None`: `_readOnlyError("#method")` + fn create_private_getter( + &self, + private_name: &str, + class_binding: Option<&BoundIdentifier<'a>>, + prop_binding: Option<&BoundIdentifier<'a>>, + object: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let Some(prop_binding) = prop_binding else { + // `_readOnlyError("#method")` + return self.create_sequence_with_write_only_error(private_name, object, span, ctx); + }; + let prop_ident = prop_binding.create_read_expression(ctx); + if let Some(class_binding) = class_binding { + let class_ident = class_binding.create_read_expression(ctx); + let object = self.create_assert_class_brand_without_value(class_ident, object, ctx); + // `_prop.call(_assertClassBrand(Class, object))` + create_call_call(prop_ident, object, span, ctx) + } else { + // `_privateFieldGet(_prop, object)` + self.create_private_field_get(prop_ident, object, span, ctx) + } + } + + /// * Setter: `_prop.call(_assertClassBrand(Class, object), value)` + /// * Prop: `_privateFieldSet(_prop, object, value)` + /// * Prop binding is `None`: `_writeOnlyError("#method")` + fn create_private_setter( + &self, + private_name: &str, + class_binding: Option<&BoundIdentifier<'a>>, + prop_binding: Option<&BoundIdentifier<'a>>, + object: Expression<'a>, + value: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let Some(prop_binding) = prop_binding else { + // `_writeOnlyError("#method")` + return self.create_sequence_with_read_only_error( + private_name, + object, + Some(value), + span, + ctx, + ); + }; + let prop_ident = prop_binding.create_read_expression(ctx); + if let Some(class_binding) = class_binding { + let class_ident = class_binding.create_read_expression(ctx); + let object = self.create_assert_class_brand_without_value(class_ident, object, ctx); + let arguments = ctx.ast.vec_from_array([Argument::from(object), Argument::from(value)]); + let callee = create_member_callee(prop_ident, "call", ctx); + // `_prop.call(_assertClassBrand(Class, object), value)` + ctx.ast.expression_call(span, callee, NONE, arguments, false) + } else { + // `_privateFieldSet(_prop, object, value)` + self.create_private_field_set(prop_ident, object, value, span, ctx) + } + } + + /// * [`Helper::ReadOnlyError`][]: `_readOnlyError("#method")` + /// * [`Helper::WriteOnlyError`][]: `_writeOnlyError("#method")` + fn create_throw_error( + &self, + helper: Helper, + private_name: &str, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let message = ctx.ast.atom_from_strs_array(["#", private_name]); + let message = ctx.ast.expression_string_literal(SPAN, message, None); + self.ctx.helper_call_expr(helper, SPAN, ctx.ast.vec1(Argument::from(message)), ctx) + } + + /// `object, value, _readOnlyError("#method")` + fn create_sequence_with_read_only_error( + &self, + private_name: &str, + object: Expression<'a>, + value: Option>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let has_value = value.is_some(); + let error = self.create_throw_error(Helper::ReadOnlyError, private_name, ctx); + let expressions = if let Some(value) = value { + ctx.ast.vec_from_array([object, value, error]) + } else { + ctx.ast.vec_from_array([object, error]) + }; + let expr = ctx.ast.expression_sequence(span, expressions); + if has_value { + expr + } else { + Expression::from(Self::create_underscore_member_expr(expr, span, ctx)) + } + } + + /// `object, _writeOnlyError("#method")` + fn create_sequence_with_write_only_error( + &self, + private_name: &str, + object: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let error = self.create_throw_error(Helper::WriteOnlyError, private_name, ctx); + let expressions = ctx.ast.vec_from_array([object, error]); + ctx.ast.expression_sequence(span, expressions) + } + + /// _checkInRHS(object) + fn create_check_in_rhs( + &self, + object: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + self.ctx.helper_call_expr( + Helper::CheckInRHS, + span, + ctx.ast.vec1(Argument::from(object)), + ctx, + ) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/private_method.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/private_method.rs new file mode 100644 index 000000000000..c4a31068d894 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/private_method.rs @@ -0,0 +1,172 @@ +//! ES2022: Class Properties +//! Transform of private method uses e.g. `this.#method()`. + +use oxc_allocator::TakeIn; +use oxc_ast::ast::*; +use oxc_ast_visit::{VisitMut, walk_mut}; +use oxc_semantic::ScopeFlags; +use oxc_span::SPAN; + +use crate::{Helper, context::TraverseCtx}; + +use super::{ + ClassProperties, + super_converter::{ClassPropertiesSuperConverter, ClassPropertiesSuperConverterMode}, +}; + +impl<'a> ClassProperties<'a, '_> { + /// Convert method definition where the key is a private identifier and + /// insert it after the class. + /// + /// ```js + /// class C { + /// #method() {} + /// set #prop(value) {} + /// get #prop() { return 0; } + /// } + /// ``` + /// + /// -> + /// + /// ```js + /// class C {} + /// function _method() {} + /// function _set_prop(value) {} + /// function _get_prop() { return 0; } + /// ``` + /// + /// Returns `true` if the method was converted. + pub(super) fn convert_private_method( + &mut self, + method: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let MethodDefinition { key, value, span, kind, r#static, .. } = method; + let PropertyKey::PrivateIdentifier(ident) = &key else { + return None; + }; + + let mut function = value.take_in_box(ctx.ast); + + let resolved_private_prop = if *kind == MethodDefinitionKind::Set { + self.classes_stack.find_writeable_private_prop(ident) + } else { + self.classes_stack.find_readable_private_prop(ident) + }; + let temp_binding = resolved_private_prop.unwrap().prop_binding; + + function.span = *span; + function.id = Some(temp_binding.create_binding_identifier(ctx)); + function.r#type = FunctionType::FunctionDeclaration; + + // Change parent scope of function to current scope id and remove + // strict mode flag if parent scope is not strict mode. + let scope_id = function.scope_id(); + let new_parent_id = ctx.current_scope_id(); + ctx.scoping_mut().change_scope_parent_id(scope_id, Some(new_parent_id)); + let is_strict_mode = ctx.current_scope_flags().is_strict_mode(); + let flags = ctx.scoping_mut().scope_flags_mut(scope_id); + *flags -= ScopeFlags::GetAccessor | ScopeFlags::SetAccessor; + if !is_strict_mode { + // TODO: Needs to remove all child scopes' strict mode flag if child scope + // is inherited from this scope. + *flags -= ScopeFlags::StrictMode; + } + + PrivateMethodVisitor::new(*r#static, self, ctx) + .visit_function(&mut function, ScopeFlags::Function); + + Some(Statement::FunctionDeclaration(function)) + } + + // `_classPrivateMethodInitSpec(this, brand)` + pub(super) fn create_class_private_method_init_spec( + &self, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let brand = self.classes_stack.last().bindings.brand.as_ref().unwrap(); + let arguments = ctx.ast.vec_from_array([ + Argument::from(ctx.ast.expression_this(SPAN)), + Argument::from(brand.create_read_expression(ctx)), + ]); + self.ctx.helper_call_expr(Helper::ClassPrivateMethodInitSpec, SPAN, arguments, ctx) + } +} + +/// Visitor to transform private methods. +/// +/// Almost the same as `super::static_block_and_prop_init::StaticVisitor`, +/// but only does following: +/// +/// 1. Reference to class name to class temp var. +/// 2. Transform `super` expressions. +struct PrivateMethodVisitor<'a, 'ctx, 'v> { + super_converter: ClassPropertiesSuperConverter<'a, 'ctx, 'v>, + /// `TransCtx` object. + ctx: &'v mut TraverseCtx<'a>, +} + +impl<'a, 'ctx, 'v> PrivateMethodVisitor<'a, 'ctx, 'v> { + fn new( + is_static: bool, + class_properties: &'v mut ClassProperties<'a, 'ctx>, + ctx: &'v mut TraverseCtx<'a>, + ) -> Self { + let mode = if is_static { + ClassPropertiesSuperConverterMode::StaticPrivateMethod + } else { + ClassPropertiesSuperConverterMode::PrivateMethod + }; + Self { super_converter: ClassPropertiesSuperConverter::new(mode, class_properties), ctx } + } +} + +impl<'a> VisitMut<'a> for PrivateMethodVisitor<'a, '_, '_> { + #[inline] + fn visit_expression(&mut self, expr: &mut Expression<'a>) { + match expr { + // `super.prop` + Expression::StaticMemberExpression(_) => { + self.super_converter.transform_static_member_expression(expr, self.ctx); + } + // `super[prop]` + Expression::ComputedMemberExpression(_) => { + self.super_converter.transform_computed_member_expression(expr, self.ctx); + } + // `super.prop()` + Expression::CallExpression(call_expr) => { + self.super_converter + .transform_call_expression_for_super_member_expr(call_expr, self.ctx); + } + // `super.prop = value`, `super.prop += value`, `super.prop ??= value` + Expression::AssignmentExpression(_) => { + self.super_converter + .transform_assignment_expression_for_super_assignment_target(expr, self.ctx); + } + // `super.prop++`, `--super.prop` + Expression::UpdateExpression(_) => { + self.super_converter + .transform_update_expression_for_super_assignment_target(expr, self.ctx); + } + _ => {} + } + walk_mut::walk_expression(self, expr); + } + + /// Transform reference to class name to temp var + fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) { + self.super_converter.class_properties.replace_class_name_with_temp_var(ident, self.ctx); + } + + #[inline] + fn visit_class(&mut self, _class: &mut Class<'a>) { + // Ignore because we don't need to transform `super` for other classes. + + // TODO: Actually we do need to transform `super` in: + // 1. Class decorators + // 2. Class `extends` clause + // 3. Class property/method/accessor computed keys + // 4. Class property/method/accessor decorators + // (or does `super` in a decorator refer to inner class?) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/prop_decl.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/prop_decl.rs new file mode 100644 index 000000000000..f86f00654ea2 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/prop_decl.rs @@ -0,0 +1,390 @@ +//! ES2022: Class Properties +//! Transform of class property declarations (instance or static properties). + +use oxc_ast::{NONE, ast::*}; +use oxc_span::SPAN; +use oxc_syntax::reference::ReferenceFlags; + +use crate::{ + common::helper_loader::Helper, context::TraverseCtx, utils::ast_builder::create_assignment, +}; + +use super::{ + ClassProperties, + utils::{create_underscore_ident_name, create_variable_declaration}, +}; + +// Instance properties +impl<'a> ClassProperties<'a, '_> { + /// Convert instance property to initialization expression. + /// Property `prop = 123;` -> Expression `this.prop = 123` or `_defineProperty(this, "prop", 123)`. + pub(super) fn convert_instance_property( + &mut self, + prop: &mut PropertyDefinition<'a>, + instance_inits: &mut Vec>, + ctx: &mut TraverseCtx<'a>, + ) { + // Get value + let value = prop.value.take(); + + let init_expr = if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key { + let value = value.unwrap_or_else(|| ctx.ast.void_0(SPAN)); + self.create_private_instance_init_assignment(ident, value, ctx) + } else { + let value = match value { + Some(value) => value, + // Do not need to convert property to `assignee.prop = void 0` if no initializer exists when + // `set_public_class_fields` and `remove_class_fields_without_initializer` + // are both true. + // This is to align `TypeScript` with `useDefineForClassFields: false`. + None if self.set_public_class_fields + && self.remove_class_fields_without_initializer => + { + return; + } + None => ctx.ast.void_0(SPAN), + }; + + // Convert to assignment or `_defineProperty` call, depending on `loose` option + let this = ctx.ast.expression_this(SPAN); + self.create_init_assignment(prop, value, this, false, ctx) + }; + instance_inits.push(init_expr); + } + + /// Create init assignment for private instance prop, to be inserted into class constructor. + /// + /// Loose: `Object.defineProperty(this, _prop, {writable: true, value: value})` + /// Not loose: `_classPrivateFieldInitSpec(this, _prop, value)` + fn create_private_instance_init_assignment( + &self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if self.private_fields_as_properties { + let this = ctx.ast.expression_this(SPAN); + self.create_private_init_assignment_loose(ident, value, this, ctx) + } else { + self.create_private_instance_init_assignment_not_loose(ident, value, ctx) + } + } + + /// `_classPrivateFieldInitSpec(this, _prop, value)` + fn create_private_instance_init_assignment_not_loose( + &self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let private_props = self.current_class().private_props.as_ref().unwrap(); + let prop = &private_props[&ident.name]; + let arguments = ctx.ast.vec_from_array([ + Argument::from(ctx.ast.expression_this(SPAN)), + Argument::from(prop.binding.create_read_expression(ctx)), + Argument::from(value), + ]); + // TODO: Should this have span of original `PropertyDefinition`? + self.ctx.helper_call_expr(Helper::ClassPrivateFieldInitSpec, SPAN, arguments, ctx) + } +} + +// Static properties +impl<'a> ClassProperties<'a, '_> { + /// Convert static property to initialization expression. + /// Property `static prop = 123;` -> Expression `C.prop = 123` or `_defineProperty(C, "prop", 123)`. + pub(super) fn convert_static_property( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Get value. + // Transform it to replace `this` and references to class name with temp var for class. + // Also transform `super`. + let value = prop.value.take().map(|mut value| { + self.transform_static_initializer(&mut value, ctx); + value + }); + + if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key { + let value = value.unwrap_or_else(|| ctx.ast.void_0(SPAN)); + self.insert_private_static_init_assignment(ident, value, ctx); + } else { + let value = match value { + Some(value) => value, + // Do not need to convert property to `assignee.prop = void 0` if no initializer exists when + // `set_public_class_fields` and `remove_class_fields_without_initializer` + // are both true. + // This is to align `TypeScript` with `useDefineForClassFields: false`. + None if self.set_public_class_fields + && self.remove_class_fields_without_initializer => + { + return self.extract_computed_key(prop, ctx); + } + None => ctx.ast.void_0(SPAN), + }; + + // Convert to assignment or `_defineProperty` call, depending on `loose` option + let class_details = self.current_class(); + let class_binding = if class_details.is_declaration { + // Class declarations always have a name except `export default class {}`. + // For default export, binding is created when static prop found in 1st pass. + class_details.bindings.name.as_ref().unwrap() + } else { + // Binding is created when static prop found in 1st pass. + class_details.bindings.temp.as_ref().unwrap() + }; + + let assignee = class_binding.create_read_expression(ctx); + let init_expr = self.create_init_assignment(prop, value, assignee, true, ctx); + self.insert_expr_after_class(init_expr, ctx); + } + } + + /// Insert after class: + /// + /// Not loose: + /// * Class declaration: `var _prop = {_: value};` + /// * Class expression: `_prop = {_: value}` + /// + /// Loose: + /// `Object.defineProperty(Class, _prop, {writable: true, value: value});` + fn insert_private_static_init_assignment( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.private_fields_as_properties { + self.insert_private_static_init_assignment_loose(ident, value, ctx); + } else { + self.insert_private_static_init_assignment_not_loose(ident, value, ctx); + } + } + + /// Insert after class: + /// `Object.defineProperty(Class, _prop, {writable: true, value: value});` + fn insert_private_static_init_assignment_loose( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // TODO: This logic appears elsewhere. De-duplicate it. + let class_details = self.current_class(); + let class_binding = if class_details.is_declaration { + // Class declarations always have a name except `export default class {}`. + // For default export, binding is created when static prop found in 1st pass. + class_details.bindings.name.as_ref().unwrap() + } else { + // Binding is created when static prop found in 1st pass. + class_details.bindings.temp.as_ref().unwrap() + }; + + let assignee = class_binding.create_read_expression(ctx); + let assignment = self.create_private_init_assignment_loose(ident, value, assignee, ctx); + self.insert_expr_after_class(assignment, ctx); + } + + /// Insert after class: + /// + /// * Class declaration: `var _prop = {_: value};` + /// * Class expression: `_prop = {_: value}` + fn insert_private_static_init_assignment_not_loose( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // `_prop = {_: value}` + let property = ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + PropertyKey::StaticIdentifier(ctx.ast.alloc(create_underscore_ident_name(ctx))), + value, + false, + false, + false, + ); + let obj = ctx.ast.expression_object(SPAN, ctx.ast.vec1(property)); + + // Insert after class + let class_details = self.current_class(); + let private_props = class_details.private_props.as_ref().unwrap(); + let prop_binding = &private_props[&ident.name].binding; + + if class_details.is_declaration { + // `var _prop = {_: value};` + let var_decl = create_variable_declaration(prop_binding, obj, ctx); + self.insert_after_stmts.push(var_decl); + } else { + // `_prop = {_: value}` + let assignment = create_assignment(prop_binding, obj, ctx); + self.insert_after_exprs.push(assignment); + } + } +} + +// Used for both instance and static properties +impl<'a> ClassProperties<'a, '_> { + /// `assignee.prop = value` or `_defineProperty(assignee, "prop", value)` + /// `#[inline]` because the caller has been checked `self.set_public_class_fields`. + /// After inlining, the two `self.set_public_class_fields` checks may be folded into one. + #[inline] + fn create_init_assignment( + &mut self, + prop: &mut PropertyDefinition<'a>, + value: Expression<'a>, + assignee: Expression<'a>, + is_static: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if self.set_public_class_fields { + // `assignee.prop = value` + self.create_init_assignment_loose(prop, value, assignee, is_static, ctx) + } else { + // `_defineProperty(assignee, "prop", value)` + self.create_init_assignment_not_loose(prop, value, assignee, is_static, ctx) + } + } + + /// `this.prop = value` or `_Class.prop = value` + fn create_init_assignment_loose( + &mut self, + prop: &mut PropertyDefinition<'a>, + value: Expression<'a>, + assignee: Expression<'a>, + is_static: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // In-built static props `name` and `length` need to be set with `_defineProperty` + let needs_define = |name| is_static && (name == "name" || name == "length"); + + let left = match &mut prop.key { + PropertyKey::StaticIdentifier(ident) => { + if needs_define(&ident.name) { + return self + .create_init_assignment_not_loose(prop, value, assignee, is_static, ctx); + } + ctx.ast.member_expression_static(SPAN, assignee, ident.as_ref().clone(), false) + } + PropertyKey::StringLiteral(str_lit) if needs_define(&str_lit.value) => { + return self + .create_init_assignment_not_loose(prop, value, assignee, is_static, ctx); + } + key @ match_expression!(PropertyKey) => { + let key = key.to_expression_mut(); + // Note: Key can also be static `StringLiteral` or `NumericLiteral`. + // `class C { 'x' = true; 123 = false; }` + // No temp var is created for these. + // TODO: Any other possible static key types? + let key = self.create_computed_key_temp_var_if_required(key, is_static, ctx); + ctx.ast.member_expression_computed(SPAN, assignee, key, false) + } + PropertyKey::PrivateIdentifier(_) => { + // Handled in `convert_instance_property` and `convert_static_property` + unreachable!(); + } + }; + + // TODO: Should this have span of the original `PropertyDefinition`? + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + AssignmentTarget::from(left), + value, + ) + } + + /// `_defineProperty(this, "prop", value)` or `_defineProperty(_Class, "prop", value)` + fn create_init_assignment_not_loose( + &mut self, + prop: &mut PropertyDefinition<'a>, + value: Expression<'a>, + assignee: Expression<'a>, + is_static: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let key = match &mut prop.key { + PropertyKey::StaticIdentifier(ident) => { + ctx.ast.expression_string_literal(ident.span, ident.name, None) + } + key @ match_expression!(PropertyKey) => { + let key = key.to_expression_mut(); + // Note: Key can also be static `StringLiteral` or `NumericLiteral`. + // `class C { 'x' = true; 123 = false; }` + // No temp var is created for these. + // TODO: Any other possible static key types? + self.create_computed_key_temp_var_if_required(key, is_static, ctx) + } + PropertyKey::PrivateIdentifier(_) => { + // Handled in `convert_instance_property` and `convert_static_property` + unreachable!(); + } + }; + + let arguments = ctx.ast.vec_from_array([ + Argument::from(assignee), + Argument::from(key), + Argument::from(value), + ]); + // TODO: Should this have span of the original `PropertyDefinition`? + self.ctx.helper_call_expr(Helper::DefineProperty, SPAN, arguments, ctx) + } + + /// `Object.defineProperty(, _prop, {writable: true, value: value})` + fn create_private_init_assignment_loose( + &self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + assignee: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // `Object.defineProperty` + let object_symbol_id = ctx.scoping().find_binding(ctx.current_scope_id(), "Object"); + let object = ctx.create_ident_expr( + SPAN, + Atom::from("Object"), + object_symbol_id, + ReferenceFlags::Read, + ); + let property = ctx.ast.identifier_name(SPAN, "defineProperty"); + let callee = + Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)); + + // `{writable: true, value: }` + let prop_def = ctx.ast.expression_object( + SPAN, + ctx.ast.vec_from_array([ + ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + ctx.ast.property_key_static_identifier(SPAN, Atom::from("writable")), + ctx.ast.expression_boolean_literal(SPAN, true), + false, + false, + false, + ), + ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + ctx.ast.property_key_static_identifier(SPAN, Atom::from("value")), + value, + false, + false, + false, + ), + ]), + ); + + let private_props = self.current_class().private_props.as_ref().unwrap(); + let prop_binding = &private_props[&ident.name].binding; + let arguments = ctx.ast.vec_from_array([ + Argument::from(assignee), + Argument::from(prop_binding.create_read_expression(ctx)), + Argument::from(prop_def), + ]); + // TODO: Should this have span of original `PropertyDefinition`? + ctx.ast.expression_call(SPAN, callee, NONE, arguments, false) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/static_block_and_prop_init.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/static_block_and_prop_init.rs new file mode 100644 index 000000000000..678fd9a83cbd --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/static_block_and_prop_init.rs @@ -0,0 +1,549 @@ +//! ES2022: Class Properties +//! Transform of static property initializers and static blocks. + +use std::cell::Cell; + +use oxc_allocator::TakeIn; +use oxc_ast::ast::*; +use oxc_ast_visit::{VisitMut, walk_mut}; +use oxc_syntax::scope::{ScopeFlags, ScopeId}; + +use crate::{context::TraverseCtx, utils::ast_builder::wrap_statements_in_arrow_function_iife}; + +use super::{ + ClassProperties, + super_converter::{ClassPropertiesSuperConverter, ClassPropertiesSuperConverterMode}, +}; + +impl<'a> ClassProperties<'a, '_> { + /// Transform static property initializer. + /// + /// Replace `this`, and references to class name, with temp var for class. Transform `super`. + /// See below for full details of transforms. + pub(super) fn transform_static_initializer( + &mut self, + value: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let make_sloppy_mode = !ctx.current_scope_flags().is_strict_mode(); + let mut replacer = StaticVisitor::new(make_sloppy_mode, true, self, ctx); + replacer.visit_expression(value); + } + + /// Transform static block. + /// + /// Transform to an `Expression` and insert after class body. + /// + /// `static { x = 1; }` -> `x = 1` + /// `static { x = 1; y = 2; } -> `(() => { x = 1; y = 2; })()` + /// + /// Replace `this`, and references to class name, with temp var for class. Transform `super`. + /// See below for full details of transforms. + /// + /// TODO: Add tests for this if there aren't any already. + /// Include tests for evaluation order inc that static block goes before class expression + /// unless also static properties, or static block uses class name. + pub(super) fn convert_static_block( + &mut self, + block: &mut StaticBlock<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let replacement = self.convert_static_block_to_expression(block, ctx); + self.insert_expr_after_class(replacement, ctx); + } + + fn convert_static_block_to_expression( + &mut self, + block: &mut StaticBlock<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let scope_id = block.scope_id(); + let outer_scope_strict_flag = ctx.current_scope_flags() & ScopeFlags::StrictMode; + let make_sloppy_mode = outer_scope_strict_flag == ScopeFlags::empty(); + + // 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 + && let Statement::ExpressionStatement(stmt) = stmts.first_mut().unwrap() + { + return self.convert_static_block_with_single_expression_to_expression( + &mut stmt.expression, + scope_id, + make_sloppy_mode, + ctx, + ); + } + + // Wrap statements in an IIFE. + // Note: Do not reparent scopes. + let mut replacer = StaticVisitor::new(make_sloppy_mode, false, self, ctx); + replacer.visit_statements(stmts); + + let scope_flags = outer_scope_strict_flag | ScopeFlags::Function | ScopeFlags::Arrow; + *ctx.scoping_mut().scope_flags_mut(scope_id) = scope_flags; + + let outer_scope_id = ctx.current_scope_id(); + ctx.scoping_mut().change_scope_parent_id(scope_id, Some(outer_scope_id)); + + wrap_statements_in_arrow_function_iife(stmts.take_in(ctx.ast), scope_id, block.span, ctx) + } + + fn convert_static_block_with_single_expression_to_expression( + &mut self, + expr: &mut Expression<'a>, + scope_id: ScopeId, + make_sloppy_mode: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // Note: Reparent scopes + let mut replacer = StaticVisitor::new(make_sloppy_mode, true, self, ctx); + replacer.visit_expression(expr); + + // Delete scope for static block + ctx.scoping_mut().delete_scope(scope_id); + + expr.take_in(ctx.ast) + } + + /// Replace reference to class name with reference to temp var for class. + pub(super) fn replace_class_name_with_temp_var( + &mut self, + ident: &mut IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Check identifier is reference to class name + let class_details = self.current_class_mut(); + let class_name_symbol_id = class_details.bindings.name_symbol_id(); + let Some(class_name_symbol_id) = class_name_symbol_id else { return }; + + let reference_id = ident.reference_id(); + let reference = ctx.scoping().get_reference(reference_id); + let Some(symbol_id) = reference.symbol_id() else { return }; + + if symbol_id != class_name_symbol_id { + return; + } + + // Identifier is reference to class name. Rename it. + let temp_binding = class_details.bindings.get_or_init_static_binding(ctx); + ident.name = temp_binding.name; + + let symbols = ctx.scoping_mut(); + symbols.get_reference_mut(reference_id).set_symbol_id(temp_binding.symbol_id); + symbols.delete_resolved_reference(symbol_id, reference_id); + symbols.add_resolved_reference(temp_binding.symbol_id, reference_id); + } +} + +/// Visitor to transform: +/// +/// 1. `this` to class temp var. +/// * Class declaration: +/// * `class C { static x = this.y; }` -> `var _C; class C {}; _C = C; C.x = _C.y;` +/// * `class C { static { this.x(); } }` -> `var _C; class C {}; _C = C; _C.x();` +/// * Class expression: +/// * `x = class C { static x = this.y; }` -> `var _C; x = (_C = class C {}, _C.x = _C.y, _C)` +/// * `C = class C { static { this.x(); } }` -> `var _C; C = (_C = class C {}, _C.x(), _C)` +/// 2. Reference to class name to class temp var. +/// * Class declaration: +/// * `class C { static x = C.y; }` -> `var _C; class C {}; _C = C; C.x = _C.y;` +/// * `class C { static { C.x(); } }` -> `var _C; class C {}; _C = C; _C.x();` +/// * Class expression: +/// * `x = class C { static x = C.y; }` -> `var _C; x = (_C = class C {}, _C.x = _C.y, _C)` +/// * `x = class C { static { C.x(); } }` -> `var _C; x = (_C = class C {}, _C.x(), _C)` +/// 3. `super` to transpiled super. +/// * e.g. `super.prop` -> `_superPropGet(_Class, "prop", this)` (in static private method) +/// or `_superPropGet(_Class, "prop", _Class)` (in static property initializer or static block) +/// +/// Also: +/// * Update parent `ScopeId` of first level of scopes, if `reparent_scopes == true`. +/// * Set `ScopeFlags` of scopes to sloppy mode if code outside the class is sloppy mode. +/// +/// Reason we need to transform `this` is because the initializer/block is being moved from inside +/// the class to outside. `this` outside the class refers to a different `this`. So we need to transform it. +/// +/// Note that for class declarations, assignments are made to properties of original class name `C`, +/// but temp var `_C` is used in replacements for `this` or class name. +/// This is because class binding `C` could be mutated, and the initializer/block may contain functions +/// which are not executed immediately, so the mutation occurs before that code runs. +/// +/// ```js +/// class C { +/// static getSelf = () => this; +/// static getSelf2 = () => C; +/// } +/// const C2 = C; +/// C = 123; +/// assert(C2.getSelf() === C); // Would fail if `this` was replaced with `C`, instead of temp var +/// assert(C2.getSelf2() === C); // Would fail if `C` in `getSelf2` was not replaced with temp var +/// ``` +/// +/// If this class has no name, and no `ScopeFlags` need updating, then we only need to transform `this`, +/// and re-parent first-level scopes. So can skip traversing into functions and other contexts which have +/// their own `this`. +// +// TODO(improve-on-babel): Unnecessary to create temp var for class declarations if either: +// 1. Class name binding is not mutated. +// 2. `this` / reference to class name / private field is not in a nested function, so we know the +// code runs immediately, before any mutation of the class name binding can occur. +// +// TODO(improve-on-babel): Updating `ScopeFlags` for strict mode makes semantic correctly for the output, +// but actually the transform isn't right. Should wrap initializer/block in a strict mode IIFE so that +// code runs in strict mode, as it was before within class body. +struct StaticVisitor<'a, 'ctx, 'v> { + /// `true` if class has name, or `ScopeFlags` need updating. + /// Either of these neccesitates walking the whole tree. If neither applies, we only need to walk + /// as far as functions and other constructs which define a `this`. + walk_deep: bool, + /// `true` if should make scopes sloppy mode + make_sloppy_mode: bool, + /// Incremented when entering a different `this` context, decremented when exiting it. + /// `this` should be transformed when `this_depth == 0`. + this_depth: u32, + /// Incremented when entering scope, decremented when exiting it. + /// Parent `ScopeId` should be updated when `scope_depth == 0`. + /// Note: `scope_depth` does not aim to track scope depth completely accurately. + /// Only requirement is to ensure that `scope_depth == 0` only when we're in first-level scope. + /// So we don't bother incrementing + decrementing for scopes which are definitely not first level. + /// In a static property initializer, e.g. `BlockStatement` or `ForStatement` must be in a function, + /// and therefore we're already in a nested scope. + /// In a static block which contains statements, we're wrapping it in an IIFE which takes on + /// the `ScopeId` of the old static block, so we don't need to reparent scopes anyway, + /// so `scope_depth` is ignored. + scope_depth: u32, + /// Converter for `super` expressions. + super_converter: ClassPropertiesSuperConverter<'a, 'ctx, 'v>, + /// `TransCtx` object. + ctx: &'v mut TraverseCtx<'a>, +} + +impl<'a, 'ctx, 'v> StaticVisitor<'a, 'ctx, 'v> { + fn new( + make_sloppy_mode: bool, + reparent_scopes: bool, + class_properties: &'v mut ClassProperties<'a, 'ctx>, + ctx: &'v mut TraverseCtx<'a>, + ) -> Self { + let walk_deep = + make_sloppy_mode || class_properties.current_class().bindings.name.is_some(); + + // Set `scope_depth` to 1 initially if don't need to reparent scopes + // (static block where converting to IIFE) + #[expect(clippy::bool_to_int_with_if)] + let scope_depth = if reparent_scopes { 0 } else { 1 }; + + Self { + walk_deep, + make_sloppy_mode, + this_depth: 0, + scope_depth, + super_converter: ClassPropertiesSuperConverter::new( + ClassPropertiesSuperConverterMode::Static, + class_properties, + ), + ctx, + } + } +} + +impl<'a> VisitMut<'a> for StaticVisitor<'a, '_, '_> { + #[inline] + fn visit_expression(&mut self, expr: &mut Expression<'a>) { + match expr { + // `this` + Expression::ThisExpression(this_expr) => { + let span = this_expr.span; + self.replace_this_with_temp_var(expr, span); + return; + } + // `delete this` + Expression::UnaryExpression(unary_expr) => { + if unary_expr.operator == UnaryOperator::Delete + && matches!(&unary_expr.argument, Expression::ThisExpression(_)) + { + let span = unary_expr.span; + self.replace_delete_this_with_true(expr, span); + return; + } + } + // `super.prop` + Expression::StaticMemberExpression(_) if self.this_depth == 0 => { + self.super_converter.transform_static_member_expression(expr, self.ctx); + } + // `super[prop]` + Expression::ComputedMemberExpression(_) if self.this_depth == 0 => { + self.super_converter.transform_computed_member_expression(expr, self.ctx); + } + // `super.prop()` + Expression::CallExpression(call_expr) if self.this_depth == 0 => { + self.super_converter + .transform_call_expression_for_super_member_expr(call_expr, self.ctx); + } + // `super.prop = value`, `super.prop += value`, `super.prop ??= value` + Expression::AssignmentExpression(_) if self.this_depth == 0 => { + self.super_converter + .transform_assignment_expression_for_super_assignment_target(expr, self.ctx); + } + // `super.prop++`, `--super.prop` + Expression::UpdateExpression(_) if self.this_depth == 0 => { + self.super_converter + .transform_update_expression_for_super_assignment_target(expr, self.ctx); + } + _ => {} + } + + walk_mut::walk_expression(self, expr); + } + + /// Transform reference to class name to temp var + fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) { + self.super_converter.class_properties.replace_class_name_with_temp_var(ident, self.ctx); + } + + /// Convert scope to sloppy mode if `self.make_sloppy_mode == true`. + // `#[inline]` because called from many `walk` functions and is small. + #[inline] + fn enter_scope(&mut self, _flags: ScopeFlags, scope_id: &Cell>) { + if self.make_sloppy_mode { + let scope_id = scope_id.get().unwrap(); + *self.ctx.scoping_mut().scope_flags_mut(scope_id) -= ScopeFlags::StrictMode; + } + } + + // Increment `this_depth` when entering code where `this` refers to a different `this` + // from `this` within this class, and decrement it when exiting. + // Therefore `this_depth == 0` when `this` refers to the `this` which needs to be transformed. + // + // Or, if class has no name, and `ScopeFlags` don't need updating, stop traversing entirely. + // No scopes need flags updating, so no point searching for them. + // + // Also set `make_sloppy_mode = false` while traversing a construct which is strict mode. + + #[inline] + fn visit_function(&mut self, func: &mut Function<'a>, flags: ScopeFlags) { + let parent_sloppy_mode = self.make_sloppy_mode; + if self.make_sloppy_mode && func.has_use_strict_directive() { + // Function has a `"use strict"` directive in body + self.make_sloppy_mode = false; + } + + self.reparent_scope_if_first_level(&func.scope_id); + + if self.walk_deep { + self.this_depth += 1; + self.scope_depth += 1; + walk_mut::walk_function(self, func, flags); + self.this_depth -= 1; + self.scope_depth -= 1; + } + + self.make_sloppy_mode = parent_sloppy_mode; + } + + #[inline] + fn visit_arrow_function_expression(&mut self, func: &mut ArrowFunctionExpression<'a>) { + let parent_sloppy_mode = self.make_sloppy_mode; + if self.make_sloppy_mode && func.has_use_strict_directive() { + // Arrow function has a `"use strict"` directive in body + self.make_sloppy_mode = false; + } + + self.reparent_scope_if_first_level(&func.scope_id); + + self.scope_depth += 1; + walk_mut::walk_arrow_function_expression(self, func); + self.scope_depth -= 1; + + self.make_sloppy_mode = parent_sloppy_mode; + } + + #[inline] + fn visit_class(&mut self, class: &mut Class<'a>) { + let parent_sloppy_mode = self.make_sloppy_mode; + // Classes are always strict mode + self.make_sloppy_mode = false; + + self.reparent_scope_if_first_level(&class.scope_id); + + // TODO: Need to visit decorators *before* incrementing `scope_depth`. + // Decorators could contain scopes. e.g. `@(() => {}) class C {}` + self.scope_depth += 1; + walk_mut::walk_class(self, class); + self.scope_depth -= 1; + + self.make_sloppy_mode = parent_sloppy_mode; + } + + #[inline] + fn visit_static_block(&mut self, block: &mut StaticBlock<'a>) { + // Not possible that `self.scope_depth == 0` here, because a `StaticBlock` + // can only be in a class, and that class would be the first-level scope. + // So no need to call `reparent_scope_if_first_level`. + + // `walk_deep` must be `true` or we couldn't get here, because a `StaticBlock` + // must be in a class, and traversal would have stopped in `visit_class` if it wasn't + self.this_depth += 1; + walk_mut::walk_static_block(self, block); + self.this_depth -= 1; + } + + #[inline] + fn visit_ts_module_block(&mut self, block: &mut TSModuleBlock<'a>) { + // Not possible that `self.scope_depth == 0` here, because a `TSModuleBlock` + // can only be in a function, and that function would be the first-level scope. + // So no need to call `reparent_scope_if_first_level`. + + let parent_sloppy_mode = self.make_sloppy_mode; + if self.make_sloppy_mode && block.has_use_strict_directive() { + // Block has a `"use strict"` directive in body + self.make_sloppy_mode = false; + } + + if self.walk_deep { + self.this_depth += 1; + walk_mut::walk_ts_module_block(self, block); + self.this_depth -= 1; + } + + self.make_sloppy_mode = parent_sloppy_mode; + } + + #[inline] + fn visit_property_definition(&mut self, prop: &mut PropertyDefinition<'a>) { + // `this` in computed key of property or method refers to `this` of parent class. + // So visit computed `key` within current `this` scope, + // but increment `this_depth` before visiting `value`. + // ```js + // class Outer { + // static prop = class Inner { [this] = 1; }; + // } + // ``` + // Don't visit `type_annotation` field because can't contain `this`. + + // Not possible that `self.scope_depth == 0` here, because a `PropertyDefinition` + // can only be in a class, and that class would be the first-level scope. + // So no need to call `reparent_scope_if_first_level`. + + // TODO: Are decorators in scope? + self.visit_decorators(&mut prop.decorators); + if prop.computed { + self.visit_property_key(&mut prop.key); + } + + // `walk_deep` must be `true` or we couldn't get here, because a `PropertyDefinition` + // must be in a class, and traversal would have stopped in `visit_class` if it wasn't + if let Some(value) = &mut prop.value { + self.this_depth += 1; + self.visit_expression(value); + self.this_depth -= 1; + } + } + + #[inline] + fn visit_accessor_property(&mut self, prop: &mut AccessorProperty<'a>) { + // Not possible that `self.scope_depth == 0` here, because an `AccessorProperty` + // can only be in a class, and that class would be the first-level scope. + // So no need to call `reparent_scope_if_first_level`. + + // Treat `key` and `value` in same way as `visit_property_definition` above. + // TODO: Are decorators in scope? + self.visit_decorators(&mut prop.decorators); + if prop.computed { + self.visit_property_key(&mut prop.key); + } + + // `walk_deep` must be `true` or we couldn't get here, because an `AccessorProperty` + // must be in a class, and traversal would have stopped in `visit_class` if it wasn't + if let Some(value) = &mut prop.value { + self.this_depth += 1; + self.visit_expression(value); + self.this_depth -= 1; + } + } + + // Remaining visitors are the only other types which have a scope which can be first-level + // when starting traversal from an `Expression`. + // + // In a static property initializer, `BlockStatement` and all other statements would need to be + // within a function, and that function would be the first-level scope. + // + // In a static block which contains statements, we're wrapping it in an IIFE which takes on + // the `ScopeId` of the old static block, so we don't need to reparent scopes anyway. + + #[inline] + fn visit_ts_conditional_type(&mut self, conditional: &mut TSConditionalType<'a>) { + self.reparent_scope_if_first_level(&conditional.scope_id); + + // `check_type` field is outside `TSConditionalType`'s scope + self.visit_ts_type(&mut conditional.check_type); + + self.enter_scope(ScopeFlags::empty(), &conditional.scope_id); + + self.scope_depth += 1; + self.visit_ts_type(&mut conditional.extends_type); + self.visit_ts_type(&mut conditional.true_type); + self.scope_depth -= 1; + + // `false_type` field is outside `TSConditionalType`'s scope + self.visit_ts_type(&mut conditional.false_type); + } + + #[inline] + fn visit_ts_method_signature(&mut self, signature: &mut TSMethodSignature<'a>) { + self.reparent_scope_if_first_level(&signature.scope_id); + + self.scope_depth += 1; + walk_mut::walk_ts_method_signature(self, signature); + self.scope_depth -= 1; + } + + #[inline] + fn visit_ts_construct_signature_declaration( + &mut self, + signature: &mut TSConstructSignatureDeclaration<'a>, + ) { + self.reparent_scope_if_first_level(&signature.scope_id); + + self.scope_depth += 1; + walk_mut::walk_ts_construct_signature_declaration(self, signature); + self.scope_depth -= 1; + } + + #[inline] + fn visit_ts_mapped_type(&mut self, mapped: &mut TSMappedType<'a>) { + self.reparent_scope_if_first_level(&mapped.scope_id); + + self.scope_depth += 1; + walk_mut::walk_ts_mapped_type(self, mapped); + self.scope_depth -= 1; + } +} + +impl<'a> StaticVisitor<'a, '_, '_> { + /// Replace `this` with reference to temp var for class. + fn replace_this_with_temp_var(&mut self, expr: &mut Expression<'a>, span: Span) { + if self.this_depth == 0 { + let class_details = self.super_converter.class_properties.current_class_mut(); + let temp_binding = class_details.bindings.get_or_init_static_binding(self.ctx); + *expr = temp_binding.create_spanned_read_expression(span, self.ctx); + } + } + + /// Replace `delete this` with `true`. + fn replace_delete_this_with_true(&self, expr: &mut Expression<'a>, span: Span) { + if self.this_depth == 0 { + *expr = self.ctx.ast.expression_boolean_literal(span, true); + } + } + + /// Update parent of scope to scope above class if this is a first-level scope. + fn reparent_scope_if_first_level(&mut self, scope_id: &Cell>) { + if self.scope_depth == 0 { + let scope_id = scope_id.get().unwrap(); + let current_scope_id = self.ctx.current_scope_id(); + self.ctx.scoping_mut().change_scope_parent_id(scope_id, Some(current_scope_id)); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/super_converter.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/super_converter.rs new file mode 100644 index 000000000000..179069915a30 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/super_converter.rs @@ -0,0 +1,629 @@ +//! ES2022: Class Properties +//! Transform of `super` expressions. + +use oxc_allocator::{Box as ArenaBox, TakeIn, Vec as ArenaVec}; +use oxc_ast::ast::*; +use oxc_span::SPAN; +use oxc_traverse::ast_operations::get_var_name_from_node; + +use crate::{ + Helper, + context::TraverseCtx, + utils::ast_builder::{create_assignment, create_prototype_member}, +}; + +use super::ClassProperties; + +#[derive(Debug)] +pub(super) enum ClassPropertiesSuperConverterMode { + // `static prop` or `static {}` + Static, + // `#method() {}` + PrivateMethod, + // `static #method() {}` + StaticPrivateMethod, +} + +/// Convert `super` expressions. +pub(super) struct ClassPropertiesSuperConverter<'a, 'ctx, 'v> { + mode: ClassPropertiesSuperConverterMode, + pub(super) class_properties: &'v mut ClassProperties<'a, 'ctx>, +} + +impl<'a, 'ctx, 'v> ClassPropertiesSuperConverter<'a, 'ctx, 'v> { + pub(super) fn new( + mode: ClassPropertiesSuperConverterMode, + class_properties: &'v mut ClassProperties<'a, 'ctx>, + ) -> Self { + Self { mode, class_properties } + } +} + +impl<'a> ClassPropertiesSuperConverter<'a, '_, '_> { + /// Transform static member expression where object is `super`. + /// + /// `super.prop` -> `_superPropGet(_Class, "prop", _Class)` + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::StaticMemberExpression`. + #[inline] + pub(super) fn transform_static_member_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::StaticMemberExpression(member) = expr else { unreachable!() }; + if member.object.is_super() { + *expr = self.transform_static_member_expression_impl(member, false, ctx); + } + } + + fn transform_static_member_expression_impl( + &mut self, + member: &StaticMemberExpression<'a>, + is_callee: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let property = &member.property; + let property = ctx.ast.expression_string_literal(property.span, property.name, None); + self.create_super_prop_get(member.span, property, is_callee, ctx) + } + + /// Transform computed member expression where object is `super`. + /// + /// `super[prop]` -> `_superPropGet(_Class, prop, _Class)` + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::ComputedMemberExpression`. + #[inline] + pub(super) fn transform_computed_member_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::ComputedMemberExpression(member) = expr else { unreachable!() }; + if member.object.is_super() { + *expr = self.transform_computed_member_expression_impl(member, false, ctx); + } + } + + fn transform_computed_member_expression_impl( + &mut self, + member: &mut ComputedMemberExpression<'a>, + is_callee: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let property = member.expression.take_in(ctx.ast); + self.create_super_prop_get(member.span, property, is_callee, ctx) + } + + /// Transform call expression where callee contains `super`. + /// + /// `super.method()` -> `_superPropGet(_Class, "method", _Class, 2)([])` + /// `super.method(1)` -> `_superPropGet(_Class, "method", _Class, 2)([1])` + // + // `#[inline]` so can bail out fast without a function call if `callee` is not a member expression + // with `super` as member expression object (fairly rare). + // Actual transform is broken out into separate functions. + #[inline] + pub(super) fn transform_call_expression_for_super_member_expr( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + match &call_expr.callee { + Expression::StaticMemberExpression(member) if member.object.is_super() => { + self.transform_call_expression_for_super_static_member_expr(call_expr, ctx); + } + Expression::ComputedMemberExpression(member) if member.object.is_super() => { + self.transform_call_expression_for_super_computed_member_expr(call_expr, ctx); + } + _ => {} + } + } + + fn transform_call_expression_for_super_static_member_expr( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let callee = &mut call_expr.callee; + let Expression::StaticMemberExpression(member) = callee else { unreachable!() }; + *callee = self.transform_static_member_expression_impl(member, true, ctx); + Self::transform_super_call_expression_arguments(&mut call_expr.arguments, ctx); + } + + fn transform_call_expression_for_super_computed_member_expr( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let callee = &mut call_expr.callee; + let Expression::ComputedMemberExpression(member) = callee else { unreachable!() }; + *callee = self.transform_computed_member_expression_impl(member, true, ctx); + Self::transform_super_call_expression_arguments(&mut call_expr.arguments, ctx); + } + + /// [A, B, C] -> [[A, B, C]] + fn transform_super_call_expression_arguments( + arguments: &mut ArenaVec<'a, Argument<'a>>, + ctx: &TraverseCtx<'a>, + ) { + let elements = arguments.drain(..).map(ArrayExpressionElement::from); + let elements = ctx.ast.vec_from_iter(elements); + let array = ctx.ast.expression_array(SPAN, elements); + arguments.push(Argument::from(array)); + } + + /// Transform assignment expression where the left-hand side is a member expression with `super`. + /// + /// * `super.prop = value` + /// -> `_superPropSet(_Class, "prop", value, _Class, 1)` + /// * `super.prop += value` + /// -> `_superPropSet(_Class, "prop", _superPropGet(_Class, "prop", _Class) + value, _Class, 1)` + /// * `super.prop &&= value` + /// -> `_superPropGet(_Class, "prop", _Class) && _superPropSet(_Class, "prop", value, _Class, 1)` + /// + /// * `super[prop] = value` + /// -> `_superPropSet(_Class, prop, value, _Class, 1)` + /// * `super[prop] += value` + /// -> `_superPropSet(_Class, prop, _superPropGet(_Class, prop, _Class) + value, _Class, 1)` + /// * `super[prop] &&= value` + /// -> `_superPropGet(_Class, prop, _Class) && _superPropSet(_Class, prop, value, _Class, 1)` + // + // `#[inline]` so can bail out fast without a function call if `left` is not a member expression + // with `super` as member expression object (fairly rare). + // Actual transform is broken out into separate functions. + pub(super) fn transform_assignment_expression_for_super_assignment_target( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr else { unreachable!() }; + match &assign_expr.left { + AssignmentTarget::StaticMemberExpression(member) if member.object.is_super() => { + self.transform_assignment_expression_for_super_static_member_expr(expr, ctx); + } + AssignmentTarget::ComputedMemberExpression(member) if member.object.is_super() => { + self.transform_assignment_expression_for_super_computed_member_expr(expr, ctx); + } + _ => {} + } + } + + /// Transform assignment expression where the left-hand side is a static member expression + /// with `super`. + /// + /// * `super.prop = value` + /// -> `_superPropSet(_Class, "prop", value, _Class, 1)` + /// * `super.prop += value` + /// -> `_superPropSet(_Class, "prop", _superPropGet(_Class, "prop", _Class) + value, _Class, 1)` + /// * `super.prop &&= value` + /// -> `_superPropGet(_Class, "prop", _Class) && _superPropSet(_Class, "prop", value, _Class, 1)` + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::AssignmentExpression`. + #[inline] + fn transform_assignment_expression_for_super_static_member_expr( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr.take_in(ctx.ast) else { + unreachable!() + }; + let AssignmentExpression { span, operator, right: value, left } = assign_expr.unbox(); + let AssignmentTarget::StaticMemberExpression(member) = left else { unreachable!() }; + let property = + ctx.ast.expression_string_literal(member.property.span, member.property.name, None); + *expr = + self.transform_super_assignment_expression_impl(span, operator, property, value, ctx); + } + + /// Transform assignment expression where the left-hand side is a computed member expression + /// with `super` as member expr object. + /// + /// * `super[prop] = value` + /// -> `_superPropSet(_Class, prop, value, _Class, 1)` + /// * `super[prop] += value` + /// -> `_superPropSet(_Class, prop, _superPropGet(_Class, prop, _Class) + value, _Class, 1)` + /// * `super[prop] &&= value` + /// -> `_superPropGet(_Class, prop, _Class) && _superPropSet(_Class, prop, value, _Class, 1)` + /// + // `#[inline]` so that compiler sees that `expr` is an `Expression::AssignmentExpression`. + #[inline] + fn transform_assignment_expression_for_super_computed_member_expr( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::AssignmentExpression(assign_expr) = expr.take_in(ctx.ast) else { + unreachable!() + }; + let AssignmentExpression { span, operator, right: value, left } = assign_expr.unbox(); + let AssignmentTarget::ComputedMemberExpression(member) = left else { unreachable!() }; + let property = member.unbox().expression.into_inner_expression(); + *expr = + self.transform_super_assignment_expression_impl(span, operator, property, value, ctx); + } + + /// Transform assignment expression where the left-hand side is a member expression with `super` + /// as member expr object. + /// + /// * `=` -> `_superPropSet(_Class, , , _Class, 1)` + /// * `+=` -> `_superPropSet(_Class, , _superPropGet(_Class, , _Class) + , 1)` + /// * `&&=` -> `_superPropGet(_Class, , _Class) && _superPropSet(_Class, , , _Class, 1)` + fn transform_super_assignment_expression_impl( + &mut self, + span: Span, + operator: AssignmentOperator, + property: Expression<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if operator == AssignmentOperator::Assign { + // `super[prop] = value` -> `_superPropSet(_Class, prop, value, _Class, 1)` + self.create_super_prop_set(span, property, value, ctx) + } else { + // Make 2 copies of `object` + let (property1, property2) = self.class_properties.duplicate_object(property, ctx); + + if let Some(operator) = operator.to_binary_operator() { + // `super[prop] += value` + // -> `_superPropSet(_Class, prop, _superPropGet(_Class, prop, _Class) + value, _Class, 1)` + + // `_superPropGet(_Class, prop, _Class)` + let get_call = self.create_super_prop_get(SPAN, property2, false, ctx); + + // `_superPropGet(_Class, prop, _Class) + value` + let value = ctx.ast.expression_binary(SPAN, get_call, operator, value); + + // `_superPropSet(_Class, prop, _superPropGet(_Class, prop, _Class) + value, 1)` + self.create_super_prop_set(span, property1, value, ctx) + } else if let Some(operator) = operator.to_logical_operator() { + // `super[prop] &&= value` + // -> `_superPropGet(_Class, prop, _Class) && _superPropSet(_Class, prop, value, _Class, 1)` + + // `_superPropGet(_Class, prop, _Class)` + let get_call = self.create_super_prop_get(SPAN, property1, false, ctx); + + // `_superPropSet(_Class, prop, value, _Class, 1)` + let set_call = self.create_super_prop_set(span, property2, value, ctx); + + // `_superPropGet(_Class, prop, _Class) && _superPropSet(_Class, prop, value, _Class, 1)` + ctx.ast.expression_logical(span, get_call, operator, set_call) + } else { + // The above covers all types of `AssignmentOperator` + unreachable!(); + } + } + } + + /// Transform update expression where the argument is a member expression with `super`. + /// + /// * `++super.prop` or `super.prop--` + /// See [`Self::transform_update_expression_for_super_static_member_expr`] + /// + /// * `++super[prop]` or `super[prop]--` + /// See [`Self::transform_update_expression_for_super_computed_member_expr`] + // + // `#[inline]` so can bail out fast without a function call if `argument` is not a member expression + // with `super` as member expression object (fairly rare). + // Actual transform is broken out into separate functions. + pub(super) fn transform_update_expression_for_super_assignment_target( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::UpdateExpression(update_expr) = expr else { unreachable!() }; + + match &update_expr.argument { + SimpleAssignmentTarget::StaticMemberExpression(member) if member.object.is_super() => { + self.transform_update_expression_for_super_static_member_expr(expr, ctx); + } + SimpleAssignmentTarget::ComputedMemberExpression(member) + if member.object.is_super() => + { + self.transform_update_expression_for_super_computed_member_expr(expr, ctx); + } + _ => {} + } + } + + /// Transform update expression (`++` or `--`) where argument is a static member expression + /// with `super`. + /// + /// * `++super.prop` -> + /// ```js + /// _superPropSet( + /// _Outer, + /// "prop", + /// ( + /// _super$prop = _superPropGet(_Outer, "prop", _Outer), + /// ++_super$prop + /// ), + /// _Outer, + /// 1 + /// ) + /// ``` + /// + /// * `super.prop--` -> + /// ```js + /// ( + /// _superPropSet( + /// _Outer, + /// "prop", + /// ( + /// _super$prop = _superPropGet(_Outer, "prop", _Outer), + /// _super$prop2 = _super$prop--, + /// _super$prop + /// ), + /// _Outer, + /// 1 + /// ), + /// _super$prop2 + /// ) + /// ``` + /// + // `#[inline]` so that compiler sees that `expr` is an `Expression::UpdateExpression`. + #[inline] + fn transform_update_expression_for_super_static_member_expr( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::UpdateExpression(mut update_expr) = expr.take_in(ctx.ast) else { + unreachable!() + }; + let SimpleAssignmentTarget::StaticMemberExpression(member) = &mut update_expr.argument + else { + unreachable!() + }; + + let temp_var_name_base = get_var_name_from_node(member.as_ref()); + + let property = + ctx.ast.expression_string_literal(member.property.span, member.property.name, None); + + *expr = self.transform_super_update_expression_impl( + &temp_var_name_base, + update_expr, + property, + ctx, + ); + } + + /// Transform update expression (`++` or `--`) where argument is a computed member expression + /// with `super`. + /// + /// * `++super[prop]` -> + /// ```js + /// _superPropSet( + /// _Outer, + /// prop, + /// ( + /// _super$prop = _superPropGet(_Outer, prop, _Outer), + /// ++_super$prop + /// ), + /// _Outer, + /// 1 + /// ) + /// ``` + /// + /// * `super[prop]--` -> + /// ```js + /// ( + /// _superPropSet( + /// _Outer, + /// prop, + /// ( + /// _super$prop = _superPropGet(_Outer, prop, _Outer), + /// _super$prop2 = _super$prop--, + /// _super$prop + /// ), + /// _Outer, + /// 1 + /// ), + /// _super$prop2 + /// ) + /// ``` + // + // `#[inline]` so that compiler sees that `expr` is an `Expression::UpdateExpression`. + #[inline] + fn transform_update_expression_for_super_computed_member_expr( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::UpdateExpression(mut update_expr) = expr.take_in(ctx.ast) else { + unreachable!() + }; + let SimpleAssignmentTarget::ComputedMemberExpression(member) = &mut update_expr.argument + else { + unreachable!() + }; + + let temp_var_name_base = get_var_name_from_node(member.as_ref()); + + let property = member.expression.get_inner_expression_mut().take_in(ctx.ast); + + *expr = self.transform_super_update_expression_impl( + &temp_var_name_base, + update_expr, + property, + ctx, + ); + } + + /// Transform update expression (`++` or `--`) where argument is a member expression with `super`. + /// + /// * `++super[prop]` -> + /// ```js + /// _superPropSet( + /// _Outer, + /// prop, + /// ( + /// _super$prop = _superPropGet(_Outer, prop, _Outer), + /// ++_super$prop + /// ), + /// _Outer, + /// 1 + /// ) + /// ``` + /// + /// * `super[prop]--` -> + /// ```js + /// ( + /// _superPropSet( + /// _Outer, + /// prop, + /// ( + /// _super$prop = _superPropGet(_Outer, prop, _Outer), + /// _super$prop2 = _super$prop--, + /// _super$prop + /// ), + /// _Outer, + /// 1 + /// ), + /// _super$prop2 + /// ) + /// ``` + fn transform_super_update_expression_impl( + &mut self, + temp_var_name_base: &str, + mut update_expr: ArenaBox<'a, UpdateExpression<'a>>, + property: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // Make 2 copies of `property` + let (property1, property2) = self.class_properties.duplicate_object(property, ctx); + + // `_superPropGet(_Class, prop, _Class)` + let get_call = self.create_super_prop_get(SPAN, property2, false, ctx); + + // `_super$prop = _superPropGet(_Class, prop, _Class)` + let temp_binding = + self.class_properties.ctx.var_declarations.create_uid_var(temp_var_name_base, ctx); + let assignment = create_assignment(&temp_binding, get_call, ctx); + + // `++_super$prop` / `_super$prop++` (reusing existing `UpdateExpression`) + let span = update_expr.span; + let prefix = update_expr.prefix; + update_expr.span = SPAN; + update_expr.argument = temp_binding.create_read_write_simple_target(ctx); + let update_expr = Expression::UpdateExpression(update_expr); + + if prefix { + // Source = `++super$prop` (prefix `++`) + // `(_super$prop = _superPropGet(_Class, prop, _Class), ++_super$prop)` + let value = ctx + .ast + .expression_sequence(SPAN, ctx.ast.vec_from_array([assignment, update_expr])); + // `_superPropSet(_Class, prop, value, _Class, 1)` + self.create_super_prop_set(span, property1, value, ctx) + } else { + // Source = `super.prop++` (postfix `++`) + // `_super$prop2 = _super$prop++` + let temp_binding2 = + self.class_properties.ctx.var_declarations.create_uid_var(temp_var_name_base, ctx); + let assignment2 = create_assignment(&temp_binding2, update_expr, ctx); + + // `(_super$prop = _superPropGet(_Class, prop, _Class), _super$prop2 = _super$prop++, _super$prop)` + let value = ctx.ast.expression_sequence( + SPAN, + ctx.ast.vec_from_array([ + assignment, + assignment2, + temp_binding.create_read_expression(ctx), + ]), + ); + + // `_superPropSet(_Class, prop, value, _Class, 1)` + let set_call = self.create_super_prop_set(span, property1, value, ctx); + // `(_superPropSet(_Class, prop, value, _Class, 1), _super$prop2)` + ctx.ast.expression_sequence( + span, + ctx.ast.vec_from_array([set_call, temp_binding2.create_read_expression(ctx)]), + ) + } + } + + /// Member: + /// `_superPropGet(_Class, prop, _Class)` + /// + /// Callee: + /// `_superPropGet(_Class, prop, _Class, 2)` + fn create_super_prop_get( + &mut self, + span: Span, + property: Expression<'a>, + is_callee: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let (class, receiver) = self.get_class_binding_arguments(ctx); + let property = Argument::from(property); + + let arguments = if is_callee { + // `(_Class, prop, _Class, 2)` + let two = ctx.ast.expression_numeric_literal(SPAN, 2.0, None, NumberBase::Decimal); + ctx.ast.vec_from_array([class, property, receiver, Argument::from(two)]) + } else { + // `(_Class, prop, _Class)` + ctx.ast.vec_from_array([class, property, receiver]) + }; + + // `_superPropGet(_Class, prop, _Class)` or `_superPropGet(_Class, prop, _Class, 2)` + self.class_properties.ctx.helper_call_expr(Helper::SuperPropGet, span, arguments, ctx) + } + + /// `_superPropSet(_Class, prop, value, _Class, 1)` + fn create_super_prop_set( + &mut self, + span: Span, + property: Expression<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let (class, receiver) = self.get_class_binding_arguments(ctx); + let arguments = ctx.ast.vec_from_array([ + class, + Argument::from(property), + Argument::from(value), + receiver, + Argument::from(ctx.ast.expression_numeric_literal( + SPAN, + 1.0, + None, + NumberBase::Decimal, + )), + ]); + self.class_properties.ctx.helper_call_expr(Helper::SuperPropSet, span, arguments, ctx) + } + + /// * [`ClassPropertiesSuperConverterMode::Static`] + /// (_Class, _Class) + /// + /// * [`ClassPropertiesSuperConverterMode::PrivateMethod`] + /// (_Class.prototype, this) + /// + /// * [`ClassPropertiesSuperConverterMode::StaticPrivateMethod`] + /// (_Class, this) + fn get_class_binding_arguments( + &mut self, + ctx: &mut TraverseCtx<'a>, + ) -> (Argument<'a>, Argument<'a>) { + let temp_binding = + self.class_properties.current_class_mut().bindings.get_or_init_static_binding(ctx); + let mut class = temp_binding.create_read_expression(ctx); + let receiver = match self.mode { + ClassPropertiesSuperConverterMode::Static => temp_binding.create_read_expression(ctx), + ClassPropertiesSuperConverterMode::PrivateMethod => { + // TODO(improve-on-babel): `superPropGet` and `superPropSet` helper function has a flag + // to use `class.prototype` rather than `class`. We should consider using that flag here. + // + class = create_prototype_member(class, ctx); + ctx.ast.expression_this(SPAN) + } + ClassPropertiesSuperConverterMode::StaticPrivateMethod => ctx.ast.expression_this(SPAN), + }; + + (Argument::from(class), Argument::from(receiver)) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_properties/utils.rs b/crates/swc_ecma_transformer/oxc/es2022/class_properties/utils.rs new file mode 100644 index 000000000000..64973d011e5e --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_properties/utils.rs @@ -0,0 +1,60 @@ +//! ES2022: Class Properties +//! Utility functions. + +use std::path::Path; + +use oxc_ast::ast::*; +use oxc_span::SPAN; +use oxc_traverse::BoundIdentifier; + +use crate::context::TraverseCtx; + +/// Create `var` declaration. +pub(super) fn create_variable_declaration<'a>( + binding: &BoundIdentifier<'a>, + init: Expression<'a>, + ctx: &TraverseCtx<'a>, +) -> Statement<'a> { + let kind = VariableDeclarationKind::Var; + let declarator = ctx.ast.variable_declarator( + SPAN, + kind, + binding.create_binding_pattern(ctx), + Some(init), + false, + ); + Statement::from(ctx.ast.declaration_variable(SPAN, kind, ctx.ast.vec1(declarator), false)) +} + +/// Convert an iterator of `Expression`s into an iterator of `Statement::ExpressionStatement`s. +pub(super) fn exprs_into_stmts<'a, E>( + exprs: E, + ctx: &TraverseCtx<'a>, +) -> impl Iterator> +where + E: IntoIterator>, +{ + exprs.into_iter().map(|expr| ctx.ast.statement_expression(SPAN, expr)) +} + +/// Create `IdentifierName` for `_`. +pub(super) fn create_underscore_ident_name<'a>(ctx: &TraverseCtx<'a>) -> IdentifierName<'a> { + ctx.ast.identifier_name(SPAN, Atom::from("_")) +} + +/// Debug assert that an `Expression` is not `ParenthesizedExpression` or TS syntax +/// (e.g. `TSAsExpression`). +// +// `#[inline(always)]` because this is a no-op in release mode +#[expect(clippy::inline_always)] +#[inline(always)] +pub(super) fn debug_assert_expr_is_not_parenthesis_or_typescript_syntax( + expr: &Expression, + path: &Path, +) { + debug_assert!( + !(matches!(expr, Expression::ParenthesizedExpression(_)) || expr.is_typescript_syntax()), + "Should not be: {expr:?} in {}", + path.display() + ); +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/class_static_block.rs b/crates/swc_ecma_transformer/oxc/es2022/class_static_block.rs new file mode 100644 index 000000000000..1c65eeb3159a --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/class_static_block.rs @@ -0,0 +1,406 @@ +//! 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::TakeIn; +use oxc_ast::{NONE, ast::*}; +use oxc_span::SPAN; +use oxc_syntax::scope::{ScopeFlags, ScopeId}; +use oxc_traverse::Traverse; + +use crate::{ + context::TraverseCtx, state::TransformState, + utils::ast_builder::wrap_statements_in_arrow_function_iife, +}; + +pub struct ClassStaticBlock; + +impl ClassStaticBlock { + pub fn new() -> Self { + Self + } +} + +impl<'a> Traverse<'a, TransformState<'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 &mut body.body { + 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( + block.span, + PropertyDefinitionType::PropertyDefinition, + ctx.ast.vec(), + key, + NONE, + Some(expr), + false, + true, + false, + false, + false, + false, + false, + 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(); + + // 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 + && 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.scoping_mut().scope_flags_mut(scope_id) = + ScopeFlags::Function | ScopeFlags::Arrow | ScopeFlags::StrictMode; + wrap_statements_in_arrow_function_iife(stmts.take_in(ctx.ast), scope_id, block.span, ctx) + } + + /// 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 = expr.take_in(ctx.ast); + + // Remove the scope for the static block from the scope chain + ctx.remove_scope_for_expression(scope_id, &expr); + + expr + } +} + +/// 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: &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: &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 key = ctx.ast.atom_from_strs_array(["_", num_str]); + self.numbered.push(&key.as_str()[1..]); + + key + } +} + +#[cfg(test)] +mod test { + use oxc_allocator::Allocator; + use oxc_semantic::Scoping; + use oxc_traverse::ReusableTraverseCtx; + + use crate::state::TransformState; + + use super::Keys; + + macro_rules! setup { + ($ctx:ident) => { + let allocator = Allocator::default(); + let scoping = Scoping::default(); + let state = TransformState::default(); + let ctx = ReusableTraverseCtx::new(state, scoping, &allocator); + // SAFETY: Macro user only gets a `&mut TransCtx`, which cannot be abused + let mut ctx = unsafe { ctx.unwrap() }; + let $ctx = &mut ctx; + }; + } + + #[test] + fn keys_no_reserved() { + setup!(ctx); + + let mut keys = Keys::default(); + + assert_eq!(keys.get_unique(ctx), "_"); + assert_eq!(keys.get_unique(ctx), "_2"); + assert_eq!(keys.get_unique(ctx), "_3"); + assert_eq!(keys.get_unique(ctx), "_4"); + assert_eq!(keys.get_unique(ctx), "_5"); + assert_eq!(keys.get_unique(ctx), "_6"); + assert_eq!(keys.get_unique(ctx), "_7"); + assert_eq!(keys.get_unique(ctx), "_8"); + assert_eq!(keys.get_unique(ctx), "_9"); + assert_eq!(keys.get_unique(ctx), "_10"); + assert_eq!(keys.get_unique(ctx), "_11"); + assert_eq!(keys.get_unique(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(ctx), "_"); + assert_eq!(keys.get_unique(ctx), "_2"); + assert_eq!(keys.get_unique(ctx), "_3"); + } + + #[test] + fn keys_reserved_underscore() { + setup!(ctx); + + let mut keys = Keys::default(); + keys.reserve("_"); + + assert_eq!(keys.get_unique(ctx), "_2"); + assert_eq!(keys.get_unique(ctx), "_3"); + assert_eq!(keys.get_unique(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(ctx), "_"); + assert_eq!(keys.get_unique(ctx), "_3"); + assert_eq!(keys.get_unique(ctx), "_5"); + assert_eq!(keys.get_unique(ctx), "_6"); + assert_eq!(keys.get_unique(ctx), "_7"); + assert_eq!(keys.get_unique(ctx), "_8"); + assert_eq!(keys.get_unique(ctx), "_9"); + assert_eq!(keys.get_unique(ctx), "_10"); + assert_eq!(keys.get_unique(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(ctx), "_"); + assert_eq!(keys.get_unique(ctx), "_2"); + assert_eq!(keys.get_unique(ctx), "_3"); + assert_eq!(keys.get_unique(ctx), "_6"); + assert_eq!(keys.get_unique(ctx), "_7"); + assert_eq!(keys.get_unique(ctx), "_8"); + assert_eq!(keys.get_unique(ctx), "_9"); + assert_eq!(keys.get_unique(ctx), "_10"); + assert_eq!(keys.get_unique(ctx), "_11"); + assert_eq!(keys.get_unique(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(ctx), "_3"); + assert_eq!(keys.get_unique(ctx), "_5"); + assert_eq!(keys.get_unique(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(ctx), "_2"); + assert_eq!(keys.get_unique(ctx), "_3"); + assert_eq!(keys.get_unique(ctx), "_6"); + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/mod.rs b/crates/swc_ecma_transformer/oxc/es2022/mod.rs new file mode 100644 index 000000000000..79ed896f9fd7 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/mod.rs @@ -0,0 +1,154 @@ +use oxc_ast::ast::*; +use oxc_diagnostics::OxcDiagnostic; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +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<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + options: ES2022Options, + + // Plugins + class_static_block: Option, + class_properties: Option>, +} + +impl<'a, 'ctx> ES2022<'a, 'ctx> { + pub fn new( + options: ES2022Options, + remove_class_fields_without_initializer: bool, + ctx: &'ctx TransformCtx<'a>, + ) -> Self { + // Class properties transform performs the static block transform differently. + // So only enable static block transform if class properties transform is disabled. + let (class_static_block, class_properties) = + if let Some(properties_options) = options.class_properties { + let class_properties = ClassProperties::new( + properties_options, + options.class_static_block, + remove_class_fields_without_initializer, + ctx, + ); + (None, Some(class_properties)) + } else { + let class_static_block = + if options.class_static_block { Some(ClassStaticBlock::new()) } else { None }; + (class_static_block, None) + }; + Self { ctx, options, class_static_block, class_properties } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2022<'a, '_> { + #[inline] // Because this is a no-op in release mode + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.exit_program(program, ctx); + } + } + + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.enter_expression(expr, ctx); + } + } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.exit_expression(expr, ctx); + } + } + + fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { + match &mut self.class_properties { + Some(class_properties) => { + class_properties.enter_class_body(body, ctx); + } + _ => { + if let Some(class_static_block) = &mut self.class_static_block { + class_static_block.enter_class_body(body, ctx); + } + } + } + } + + fn exit_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.exit_class(class, ctx); + } + } + + fn enter_assignment_target( + &mut self, + target: &mut AssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.enter_assignment_target(target, ctx); + } + } + + fn enter_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.enter_property_definition(prop, ctx); + } + } + + fn exit_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.exit_property_definition(prop, ctx); + } + } + + fn enter_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.enter_static_block(block, ctx); + } + } + + fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.exit_static_block(block, ctx); + } + } + + fn enter_await_expression( + &mut self, + node: &mut AwaitExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.top_level_await && Self::is_top_level(ctx) { + let warning = OxcDiagnostic::warn( + "Top-level await is not available in the configured target environment.", + ) + .with_label(node.span); + self.ctx.error(warning); + } + } +} + +impl ES2022<'_, '_> { + fn is_top_level(ctx: &TraverseCtx) -> bool { + ctx.current_hoist_scope_id() == ctx.scoping().root_scope_id() + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2022/options.rs b/crates/swc_ecma_transformer/oxc/es2022/options.rs new file mode 100644 index 000000000000..3719102adb2d --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2022/options.rs @@ -0,0 +1,16 @@ +use serde::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, + + #[serde(skip)] + pub class_properties: Option, + + #[serde(skip)] + pub top_level_await: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/es2026/explicit_resource_management.rs b/crates/swc_ecma_transformer/oxc/es2026/explicit_resource_management.rs new file mode 100644 index 000000000000..c8cc20552200 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2026/explicit_resource_management.rs @@ -0,0 +1,903 @@ +//! Proposal: Explicit Resource Management +//! +//! This plugin transforms explicit resource management syntax into a series of try-catch-finally blocks. +//! +//! ## Example +//! +//! Input: +//! ```js +//! for await (using x of y) { +//! doSomethingWith(x); +//! } +//! ``` +//! +//! Output: +//! ```js +//! for await (const _x of y) +//! try { +//! var _usingCtx = babelHelpers.usingCtx(); +//! const x = _usingCtx.u(_x); +//! doSomethingWith(x); +//! } catch (_) { +//! _usingCtx.e = _; +//! } finally { +//! _usingCtx.d(); +//! } +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-explicit-resource-management](https://babeljs.io/docs/babel-plugin-transform-explicit-resource-management). +//! +//! ## References: +//! * Babel plugin implementation: +//! * Explicit Resource Management TC39 proposal: + +use std::mem; + +use rustc_hash::FxHashMap; + +use oxc_allocator::{Address, Box as ArenaBox, GetAddress, TakeIn, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_ecmascript::BoundNames; +use oxc_semantic::{ScopeFlags, ScopeId, SymbolFlags}; +use oxc_span::{Atom, SPAN}; +use oxc_traverse::{BoundIdentifier, Traverse}; + +use crate::{ + Helper, + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct ExplicitResourceManagement<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + + top_level_using: FxHashMap, +} + +impl<'a, 'ctx> ExplicitResourceManagement<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx, top_level_using: FxHashMap::default() } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ExplicitResourceManagement<'a, '_> { + /// Transform `for (using ... of ...)`, ready for `enter_statement` to do the rest. + /// + /// * `for (using x of y) {}` -> `for (const _x of y) { using x = _x; }` + /// * `for await (using x of y) {}` -> `for (const _x of y) { await using x = _x; }` + fn enter_for_of_statement( + &mut self, + for_of_stmt: &mut ForOfStatement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let for_of_stmt_scope_id = for_of_stmt.scope_id(); + let ForStatementLeft::VariableDeclaration(decl) = &mut for_of_stmt.left else { return }; + if !matches!( + decl.kind, + VariableDeclarationKind::Using | VariableDeclarationKind::AwaitUsing + ) { + return; + } + let variable_decl_kind = decl.kind; + + // `for (using x of y)` -> `for (const _x of y)` + decl.kind = VariableDeclarationKind::Const; + + let variable_declarator = decl.declarations.first_mut().unwrap(); + variable_declarator.kind = VariableDeclarationKind::Const; + + let variable_declarator_binding_ident = + variable_declarator.id.get_binding_identifier().unwrap(); + + let for_of_init_symbol_id = variable_declarator_binding_ident.symbol_id(); + let for_of_init_name = variable_declarator_binding_ident.name; + + let temp_id = ctx.generate_uid_based_on_node( + variable_declarator.id.get_binding_identifier().unwrap(), + for_of_stmt_scope_id, + SymbolFlags::ConstVariable | SymbolFlags::BlockScopedVariable, + ); + + let binding_pattern = + mem::replace(&mut variable_declarator.id, temp_id.create_binding_pattern(ctx)); + + // `using x = _x;` + let using_stmt = Statement::from(ctx.ast.declaration_variable( + SPAN, + variable_decl_kind, + ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + variable_decl_kind, + binding_pattern, + Some(temp_id.create_read_expression(ctx)), + false, + )), + false, + )); + + let scope_id = match &mut for_of_stmt.body { + Statement::BlockStatement(block) => block.scope_id(), + _ => ctx.insert_scope_below_statement_from_scope_id( + &for_of_stmt.body, + for_of_stmt.scope_id(), + ScopeFlags::empty(), + ), + }; + ctx.scoping_mut().set_symbol_scope_id(for_of_init_symbol_id, scope_id); + ctx.scoping_mut().move_binding(for_of_stmt_scope_id, scope_id, &for_of_init_name); + + if let Statement::BlockStatement(body) = &mut for_of_stmt.body { + // `for (const _x of y) { x(); }` -> `for (const _x of y) { using x = _x; x(); }` + body.body.insert(0, using_stmt); + } else { + // `for (const _x of y) x();` -> `for (const _x of y) { using x = _x; x(); }` + let old_body = for_of_stmt.body.take_in(ctx.ast); + + let new_body = ctx.ast.vec_from_array([using_stmt, old_body]); + for_of_stmt.body = ctx.ast.statement_block_with_scope_id(SPAN, new_body, scope_id); + } + } + + /// Transform class static block. + /// + /// ```js + /// class C { static { using x = y(); } } + /// ``` + /// -> + /// ```js + /// class C { + /// static { + /// try { + /// var _usingCtx = babelHelpers.usingCtx(); + /// const x = _usingCtx.u(y()); + /// } catch (_) { + /// _usingCtx.e = _; + /// } finally { + /// _usingCtx.d(); + /// } + /// } + /// } + /// ``` + fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = block.scope_id(); + if let Some((new_stmts, needs_await, using_ctx)) = + self.transform_statements(&mut block.body, scope_id, ctx) + { + let static_block_new_scope_id = ctx.insert_scope_between( + ctx.scoping().scope_parent_id(scope_id).unwrap(), + scope_id, + ScopeFlags::ClassStaticBlock, + ); + + ctx.scoping_mut().set_symbol_scope_id(using_ctx.symbol_id, static_block_new_scope_id); + ctx.scoping_mut().move_binding(scope_id, static_block_new_scope_id, &using_ctx.name); + *ctx.scoping_mut().scope_flags_mut(scope_id) = ScopeFlags::StrictMode; + + block.set_scope_id(static_block_new_scope_id); + block.body = ctx.ast.vec1(Self::create_try_stmt( + ctx.ast.block_statement_with_scope_id(SPAN, new_stmts, scope_id), + &using_ctx, + static_block_new_scope_id, + needs_await, + ctx, + )); + } + } + + /// Transform function body. + /// + /// ```js + /// function f() { + /// using x = y(); + /// } + /// ``` + /// -> + /// ```js + /// function f() { + /// try { + /// var _usingCtx = babelHelpers.usingCtx(); + /// const x = _usingCtx.u(y()); + /// } catch (_) { + /// _usingCtx.e = _; + /// } finally { + /// _usingCtx.d(); + /// } + /// } + /// ``` + fn enter_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some((new_stmts, needs_await, using_ctx)) = + self.transform_statements(&mut body.statements, ctx.current_hoist_scope_id(), ctx) + { + // FIXME: this creates the scopes in the correct place, however we never move the bindings contained + // within `new_stmts` to the new scope. + let block_stmt_scope_id = + ctx.insert_scope_below_statements(&new_stmts, ScopeFlags::empty()); + + let current_scope_id = ctx.current_scope_id(); + + body.statements = ctx.ast.vec1(Self::create_try_stmt( + ctx.ast.block_statement_with_scope_id(SPAN, new_stmts, block_stmt_scope_id), + &using_ctx, + current_scope_id, + needs_await, + ctx, + )); + } + } + + /// Transform block statement or switch statement. + /// + /// See [`Self::transform_block_statement`] and [`Self::transform_switch_statement`] + /// for transformed output. + // + // `#[inline]` because this is a hot path, and most `Statement`s are not `BlockStatement`s + // or `SwitchStatement`s. We want the common path for "nothing to do here" not to incur the cost of + // a function call. + #[inline] + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + match stmt { + Statement::BlockStatement(_) => self.transform_block_statement(stmt, ctx), + Statement::SwitchStatement(_) => self.transform_switch_statement(stmt, ctx), + _ => {} + } + } + + /// Transform try statement. + /// + /// ```js + /// try { + /// using x = y(); + /// } catch (err) { } + /// ``` + /// -> + /// ```js + /// try { + /// try { + /// var _usingCtx = babelHelpers.usingCtx(); + /// const x = _usingCtx.u(y()); + /// } catch (_) { + /// _usingCtx.e = _; + /// } finally { + /// _usingCtx.d(); + /// } + /// } catch (err) { } + /// ``` + fn enter_try_statement(&mut self, node: &mut TryStatement<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = node.block.scope_id(); + + if let Some((new_stmts, needs_await, using_ctx)) = + self.transform_statements(&mut node.block.body, scope_id, ctx) + { + let block_stmt_scope_id = ctx.insert_scope_between( + ctx.scoping().scope_parent_id(scope_id).unwrap(), + scope_id, + ScopeFlags::empty(), + ); + + node.block.body = ctx.ast.vec1(Self::create_try_stmt( + ctx.ast.block_statement_with_scope_id(SPAN, new_stmts, scope_id), + &using_ctx, + block_stmt_scope_id, + needs_await, + ctx, + )); + + let current_hoist_scope_id = ctx.current_hoist_scope_id(); + node.block.set_scope_id(block_stmt_scope_id); + ctx.scoping_mut().set_symbol_scope_id(using_ctx.symbol_id, current_hoist_scope_id); + ctx.scoping_mut().move_binding(scope_id, current_hoist_scope_id, &using_ctx.name); + + ctx.scoping_mut().change_scope_parent_id(scope_id, Some(block_stmt_scope_id)); + } + } + + /// Move any top level `using` declarations within a block statement, + /// allowing `enter_statement` to transform them. + fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.top_level_using.clear(); + if !program.body.iter().any(|stmt| match stmt { + Statement::VariableDeclaration(var_decl) => matches!( + var_decl.kind, + VariableDeclarationKind::Using | VariableDeclarationKind::AwaitUsing + ), + _ => false, + }) { + return; + } + + let program_body = program.body.take_in(ctx.ast); + + let (mut program_body, inner_block): ( + ArenaVec<'a, Statement<'a>>, + ArenaVec<'a, Statement<'a>>, + ) = program_body.into_iter().fold( + (ctx.ast.vec(), ctx.ast.vec()), + |(mut program_body, mut inner_block), mut stmt| { + let address = stmt.address(); + match stmt { + Statement::FunctionDeclaration(_) + | Statement::ImportDeclaration(_) + | Statement::ExportAllDeclaration(_) => { + program_body.push(stmt); + } + Statement::ExportDefaultDeclaration(ref mut export_default_decl) => { + let (var_id, span) = match &mut export_default_decl.declaration { + ExportDefaultDeclarationKind::ClassDeclaration(class_decl) + if class_decl.id.is_some() => + { + let id = class_decl.id.take().unwrap(); + + *ctx.scoping_mut().symbol_flags_mut(id.symbol_id()) = + SymbolFlags::FunctionScopedVariable; + + (BoundIdentifier::from_binding_ident(&id), id.span) + } + ExportDefaultDeclarationKind::FunctionDeclaration(_) => { + program_body.push(stmt); + return (program_body, inner_block); + } + _ => ( + ctx.generate_binding_in_current_scope( + Atom::from("_default"), + SymbolFlags::FunctionScopedVariable, + ), + SPAN, + ), + }; + + let decl = mem::replace( + &mut export_default_decl.declaration, + ExportDefaultDeclarationKind::NullLiteral( + ctx.ast.alloc_null_literal(SPAN), + ), + ); + + let expr = match decl { + ExportDefaultDeclarationKind::FunctionDeclaration(decl) => { + Expression::FunctionExpression(decl) + } + ExportDefaultDeclarationKind::ClassDeclaration(mut decl) => { + decl.r#type = ClassType::ClassExpression; + Expression::ClassExpression(decl) + } + _ => decl.into_expression(), + }; + + inner_block.push(Statement::VariableDeclaration( + ctx.ast.alloc_variable_declaration( + span, + VariableDeclarationKind::Var, + ctx.ast.vec1(ctx.ast.variable_declarator( + span, + VariableDeclarationKind::Var, + var_id.create_binding_pattern(ctx), + Some(expr), + false, + )), + false, + ), + )); + + program_body.push(Statement::ExportNamedDeclaration( + ctx.ast.alloc_export_named_declaration( + SPAN, + None, + ctx.ast.vec1(ctx.ast.export_specifier( + SPAN, + ModuleExportName::IdentifierReference( + var_id.create_read_reference(ctx), + ), + ctx.ast.module_export_name_identifier_name(SPAN, "default"), + ImportOrExportKind::Value, + )), + None, + ImportOrExportKind::Value, + NONE, + ), + )); + } + Statement::ExportNamedDeclaration(ref mut export_named_declaration) => { + let Some(ref mut decl) = export_named_declaration.declaration else { + program_body.push(stmt); + return (program_body, inner_block); + }; + if matches!( + decl, + Declaration::FunctionDeclaration(_) + | Declaration::TSTypeAliasDeclaration(_) + | Declaration::TSInterfaceDeclaration(_) + | Declaration::TSEnumDeclaration(_) + | Declaration::TSModuleDeclaration(_) + | Declaration::TSImportEqualsDeclaration(_) + ) { + program_body.push(stmt); + + return (program_body, inner_block); + } + + let export_specifiers = match decl.take_in(ctx.ast) { + Declaration::ClassDeclaration(class_decl) => { + let class_binding = class_decl.id.as_ref().unwrap(); + let class_binding_name = class_binding.name; + + let xx = BoundIdentifier::from_binding_ident(class_binding) + .create_read_reference(ctx); + + inner_block.push(Self::transform_class_decl(class_decl, ctx)); + + let local = ModuleExportName::IdentifierReference(xx); + let exported = ctx + .ast + .module_export_name_identifier_name(SPAN, class_binding_name); + ctx.ast.vec1(ctx.ast.export_specifier( + SPAN, + local, + exported, + ImportOrExportKind::Value, + )) + } + Declaration::VariableDeclaration(mut var_decl) => { + var_decl.kind = VariableDeclarationKind::Var; + let mut export_specifiers = ctx.ast.vec(); + + for decl in &mut var_decl.declarations { + decl.kind = VariableDeclarationKind::Var; + } + + var_decl.bound_names(&mut |ident| { + *ctx.scoping_mut().symbol_flags_mut(ident.symbol_id()) = + SymbolFlags::FunctionScopedVariable; + + export_specifiers.push( + ctx.ast.export_specifier( + SPAN, + ModuleExportName::IdentifierReference( + BoundIdentifier::from_binding_ident(ident) + .create_read_reference(ctx), + ), + ctx.ast.module_export_name_identifier_name( + SPAN, ident.name, + ), + ImportOrExportKind::Value, + ), + ); + }); + inner_block.push(Statement::VariableDeclaration(var_decl)); + export_specifiers + } + _ => unreachable!(), + }; + + program_body.push(Statement::ExportNamedDeclaration( + ctx.ast.alloc_export_named_declaration( + SPAN, + None, + export_specifiers, + None, + export_named_declaration.export_kind, + NONE, + ), + )); + } + Statement::ClassDeclaration(class_decl) => { + inner_block.push(Self::transform_class_decl(class_decl, ctx)); + } + Statement::VariableDeclaration(ref mut var_declaration) => { + if var_declaration.kind == VariableDeclarationKind::Using { + self.top_level_using.insert(address, false); + } else if var_declaration.kind == VariableDeclarationKind::AwaitUsing { + self.top_level_using.insert(address, true); + } + var_declaration.kind = VariableDeclarationKind::Var; + + for decl in &mut var_declaration.declarations { + decl.kind = VariableDeclarationKind::Var; + decl.id.bound_names(&mut |c| { + *ctx.scoping_mut().symbol_flags_mut(c.symbol_id()) = + SymbolFlags::FunctionScopedVariable; + }); + } + + inner_block.push(stmt); + } + _ => inner_block.push(stmt), + } + + (program_body, inner_block) + }, + ); + + let block_scope_id = ctx.insert_scope_below_statements(&inner_block, ScopeFlags::empty()); + program_body.push(ctx.ast.statement_block_with_scope_id(SPAN, inner_block, block_scope_id)); + + std::mem::swap(&mut program.body, &mut program_body); + } +} + +impl<'a> ExplicitResourceManagement<'a, '_> { + /// Transform block statement. + /// + /// Input: + /// ```js + /// { + /// using x = y(); + /// } + /// ``` + /// Output: + /// ```js + /// try { + /// var _usingCtx = babelHelpers.usingCtx(); + /// const x = _usingCtx.u(y()); + /// } catch (_) { + /// _usingCtx.e = _; + /// } finally { + /// _usingCtx.d(); + /// } + /// ``` + fn transform_block_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let Statement::BlockStatement(block_stmt) = stmt else { unreachable!() }; + + if let Some((new_stmts, needs_await, using_ctx)) = + self.transform_statements(&mut block_stmt.body, ctx.current_hoist_scope_id(), ctx) + { + let current_scope_id = ctx.current_scope_id(); + + *stmt = Self::create_try_stmt( + ctx.ast.block_statement_with_scope_id(SPAN, new_stmts, block_stmt.scope_id()), + &using_ctx, + current_scope_id, + needs_await, + ctx, + ); + } + } + + /// Transform switch statement. + /// + /// Input: + /// ```js + /// switch (0) { + /// case 1: + /// using foo = bar; + /// doSomethingWithFoo(foo) + /// case 2: + /// throw new Error('oops') + /// } + /// ``` + /// Output: + /// ```js + /// try { + /// var _usingCtx = babelHelpers.usingCtx(); + /// switch (0) { + /// case 1: + /// const foo = _usingCtx.u(bar); + /// doSomethingWithFoo(foo); + /// case 2: + /// throw new Error('oops'); + /// } + /// } catch (_) { + /// _usingCtx.e = _; + /// } finally { + /// _usingCtx.d(); + /// } + /// ``` + fn transform_switch_statement(&self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let mut using_ctx = None; + let mut needs_await = false; + let current_scope_id = ctx.current_scope_id(); + + let Statement::SwitchStatement(switch_stmt) = stmt else { unreachable!() }; + + let switch_stmt_scope_id = switch_stmt.scope_id(); + + for case in &mut switch_stmt.cases { + for case_stmt in &mut case.consequent { + let Statement::VariableDeclaration(var_decl) = case_stmt else { continue }; + if !matches!( + var_decl.kind, + VariableDeclarationKind::Using | VariableDeclarationKind::AwaitUsing + ) { + continue; + } + needs_await = needs_await || var_decl.kind == VariableDeclarationKind::AwaitUsing; + + var_decl.kind = VariableDeclarationKind::Const; + + using_ctx = using_ctx.or_else(|| { + Some(ctx.generate_uid( + "usingCtx", + current_scope_id, + SymbolFlags::FunctionScopedVariable, + )) + }); + + for decl in &mut var_decl.declarations { + if let Some(old_init) = decl.init.take() { + decl.init = Some( + ctx.ast.expression_call( + SPAN, + Expression::from( + ctx.ast.member_expression_static( + SPAN, + using_ctx + .as_ref() + .expect("`using_ctx` should have been set") + .create_read_expression(ctx), + ctx.ast.identifier_name( + SPAN, + if needs_await { "a" } else { "u" }, + ), + false, + ), + ), + NONE, + ctx.ast.vec1(Argument::from(old_init)), + false, + ), + ); + } + } + } + } + + let Some(using_ctx) = using_ctx else { return }; + + let block_stmt_sid = ctx.create_child_scope_of_current(ScopeFlags::empty()); + + ctx.scoping_mut().change_scope_parent_id(switch_stmt_scope_id, Some(block_stmt_sid)); + + let callee = self.ctx.helper_load(Helper::UsingCtx, ctx); + + let block = { + let vec = ctx.ast.vec_from_array([ + Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + using_ctx.create_binding_pattern(ctx), + Some(ctx.ast.expression_call(SPAN, callee, NONE, ctx.ast.vec(), false)), + false, + )), + false, + )), + stmt.take_in(ctx.ast), + ]); + + ctx.ast.block_statement_with_scope_id(SPAN, vec, block_stmt_sid) + }; + + let catch = Self::create_catch_clause(&using_ctx, current_scope_id, ctx); + let finally = Self::create_finally_block(&using_ctx, current_scope_id, needs_await, ctx); + *stmt = ctx.ast.statement_try(SPAN, block, Some(catch), Some(finally)); + } + + /// Transforms: + /// - `node` - the statements to transform + /// - `hoist_scope_id` - the hoist scope, used for generating new var bindings + /// - `ctx` - the traverse context + /// + /// Input: + /// ```js + /// { + /// using foo = bar; + /// } + /// ``` + /// + /// Output: + /// ```js + /// try { + /// var _usingCtx = babelHelpers.usingCtx(); + /// const foo = _usingCtx.u(bar); + /// } catch (_) { + /// _usingCtx.e = _; + /// } finally { + /// _usingCtx.d(); + /// } + /// ``` + /// + /// Returns `Some` if the statements were transformed, `None` otherwise. + fn transform_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + hoist_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Option<(ArenaVec<'a, Statement<'a>>, bool, BoundIdentifier<'a>)> { + let mut needs_await = false; + + let mut using_ctx = None; + + for stmt in stmts.iter_mut() { + let address = stmt.address(); + let Statement::VariableDeclaration(variable_declaration) = stmt else { continue }; + if !matches!( + variable_declaration.kind, + VariableDeclarationKind::Using | VariableDeclarationKind::AwaitUsing + ) && !self.top_level_using.contains_key(&address) + { + continue; + } + let is_await_using = variable_declaration.kind == VariableDeclarationKind::AwaitUsing + || self.top_level_using.get(&address).copied().unwrap_or(false); + needs_await = needs_await || is_await_using; + + if self.top_level_using.remove(&address).is_none() { + variable_declaration.kind = VariableDeclarationKind::Const; + } + + using_ctx = using_ctx.or_else(|| { + let binding = ctx.generate_uid( + "usingCtx", + hoist_scope_id, + SymbolFlags::FunctionScopedVariable, + ); + Some(binding) + }); + + // `using foo = bar;` -> `const foo = _usingCtx.u(bar);` + // `await using foo = bar;` -> `const foo = _usingCtx.a(bar);` + for decl in &mut variable_declaration.declarations { + if let Some(old_init) = decl.init.take() { + decl.init = Some(ctx.ast.expression_call( + SPAN, + Expression::from(ctx.ast.member_expression_static( + SPAN, + using_ctx.as_ref().unwrap().create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, if is_await_using { "a" } else { "u" }), + false, + )), + NONE, + ctx.ast.vec1(Argument::from(old_init)), + false, + )); + } + } + } + + let using_ctx = using_ctx?; + + let mut stmts = stmts.take_in(ctx.ast); + + // `var _usingCtx = babelHelpers.usingCtx();` + let callee = self.ctx.helper_load(Helper::UsingCtx, ctx); + let helper = ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + using_ctx.create_binding_pattern(ctx), + Some(ctx.ast.expression_call(SPAN, callee, NONE, ctx.ast.vec(), false)), + false, + )), + false, + ); + stmts.insert(0, Statement::from(helper)); + + Some((stmts, needs_await, using_ctx)) + } + + fn create_try_stmt( + body: BlockStatement<'a>, + using_ctx: &BoundIdentifier<'a>, + parent_scope_id: ScopeId, + needs_await: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let catch = Self::create_catch_clause(using_ctx, parent_scope_id, ctx); + let finally = Self::create_finally_block(using_ctx, parent_scope_id, needs_await, ctx); + ctx.ast.statement_try(SPAN, body, Some(catch), Some(finally)) + } + + /// `catch (_) { _usingCtx.e = _; }` + fn create_catch_clause( + using_ctx: &BoundIdentifier<'a>, + parent_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> ArenaBox<'a, CatchClause<'a>> { + // catch (_) { _usingCtx.e = _; } + // ^ catch_parameter + // ^^^^^^^^^^^^^^^^^^^^^^^^ catch_scope_id + // ^^^^^^^^^^^^^^^^^^^^ block_scope_id + let catch_scope_id = ctx.create_child_scope(parent_scope_id, ScopeFlags::CatchClause); + let block_scope_id = ctx.create_child_scope(catch_scope_id, ScopeFlags::empty()); + // We can skip using `generate_uid` here as no code within the `catch` block which can use a + // binding called `_`. `using_ctx` is a UID with prefix `_usingCtx`. + let ident = ctx.generate_binding( + Atom::from("_"), + block_scope_id, + SymbolFlags::CatchVariable | SymbolFlags::FunctionScopedVariable, + ); + + let catch_parameter = ctx.ast.catch_parameter(SPAN, ident.create_binding_pattern(ctx)); + + // `_usingCtx.e = _;` + let stmt = ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + AssignmentTarget::from(ctx.ast.member_expression_static( + SPAN, + using_ctx.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, "e"), + false, + )), + ident.create_read_expression(ctx), + ), + ); + + // `catch (_) { _usingCtx.e = _; }` + ctx.ast.alloc_catch_clause_with_scope_id( + SPAN, + Some(catch_parameter), + ctx.ast.block_statement_with_scope_id(SPAN, ctx.ast.vec1(stmt), block_scope_id), + catch_scope_id, + ) + } + + /// `{ _usingCtx.d(); }` + fn create_finally_block( + using_ctx: &BoundIdentifier<'a>, + parent_scope_id: ScopeId, + needs_await: bool, + ctx: &mut TraverseCtx<'a>, + ) -> ArenaBox<'a, BlockStatement<'a>> { + let finally_scope_id = ctx.create_child_scope(parent_scope_id, ScopeFlags::empty()); + + // `_usingCtx.d()` + let expr = ctx.ast.expression_call( + SPAN, + Expression::from(ctx.ast.member_expression_static( + SPAN, + using_ctx.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, "d"), + false, + )), + NONE, + ctx.ast.vec(), + false, + ); + + let stmt = if needs_await { ctx.ast.expression_await(SPAN, expr) } else { expr }; + + ctx.ast.alloc_block_statement_with_scope_id( + SPAN, + ctx.ast.vec1(ctx.ast.statement_expression(SPAN, stmt)), + finally_scope_id, + ) + } + + /// `class C {}` -> `var C = class {};` + fn transform_class_decl( + mut class_decl: ArenaBox<'a, Class<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let id = class_decl.id.take().expect("ClassDeclaration should have an id"); + + class_decl.r#type = ClassType::ClassExpression; + let class_expr = Expression::ClassExpression(class_decl); + + *ctx.scoping_mut().symbol_flags_mut(id.symbol_id()) = SymbolFlags::FunctionScopedVariable; + + Statement::VariableDeclaration(ctx.ast.alloc_variable_declaration( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.binding_pattern( + BindingPatternKind::BindingIdentifier(ctx.ast.alloc(id)), + NONE, + false, + ), + Some(class_expr), + false, + )), + false, + )) + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2026/mod.rs b/crates/swc_ecma_transformer/oxc/es2026/mod.rs new file mode 100644 index 000000000000..3dfb7e77d910 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2026/mod.rs @@ -0,0 +1,70 @@ +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +mod explicit_resource_management; +mod options; + +use explicit_resource_management::ExplicitResourceManagement; +pub use options::ES2026Options; + +pub struct ES2026<'a, 'ctx> { + explicit_resource_management: Option>, +} + +impl<'a, 'ctx> ES2026<'a, 'ctx> { + pub fn new(options: ES2026Options, ctx: &'ctx TransformCtx<'a>) -> Self { + let explicit_resource_management = if options.explicit_resource_management { + Some(ExplicitResourceManagement::new(ctx)) + } else { + None + }; + Self { explicit_resource_management } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ES2026<'a, '_> { + fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(explicit_resource_management) = &mut self.explicit_resource_management { + explicit_resource_management.enter_program(program, ctx); + } + } + + fn enter_for_of_statement( + &mut self, + for_of_stmt: &mut ForOfStatement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(explicit_resource_management) = &mut self.explicit_resource_management { + explicit_resource_management.enter_for_of_statement(for_of_stmt, ctx); + } + } + + fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(explicit_resource_management) = &mut self.explicit_resource_management { + explicit_resource_management.exit_static_block(block, ctx); + } + } + + fn enter_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(explicit_resource_management) = &mut self.explicit_resource_management { + explicit_resource_management.enter_function_body(body, ctx); + } + } + + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(explicit_resource_management) = &mut self.explicit_resource_management { + explicit_resource_management.enter_statement(stmt, ctx); + } + } + + fn enter_try_statement(&mut self, node: &mut TryStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(explicit_resource_management) = &mut self.explicit_resource_management { + explicit_resource_management.enter_try_statement(node, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/es2026/options.rs b/crates/swc_ecma_transformer/oxc/es2026/options.rs new file mode 100644 index 000000000000..6c9d341c1097 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/es2026/options.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2026Options { + #[serde(skip)] + pub explicit_resource_management: bool, +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/comments.rs b/crates/swc_ecma_transformer/oxc/jsx/comments.rs new file mode 100644 index 000000000000..0959005f83d2 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/comments.rs @@ -0,0 +1,263 @@ +use std::borrow::Cow; + +use memchr::memchr; + +use oxc_ast::Comment; + +use crate::{JsxOptions, JsxRuntime, TransformCtx, TypeScriptOptions}; + +/// Scan through all comments and find the following pragmas: +/// +/// * @jsx Preact.h +/// * @jsxRuntime classic / automatic +/// * @jsxImportSource custom-jsx-library +/// * @jsxFrag Preact.Fragment +/// +/// The comment does not need to be a JSDoc comment, +/// otherwise `JSDoc` could be used instead. +/// +/// This behavior is aligned with ESBuild. +/// Babel is less liberal - it doesn't accept multiple pragmas in a single line +/// e.g. `/** @jsx h @jsxRuntime classic */` +/// +pub fn update_options_with_comments( + comments: &[Comment], + typescript: &mut TypeScriptOptions, + jsx: &mut JsxOptions, + ctx: &TransformCtx, +) { + let source_text = ctx.source_text; + for comment in comments { + update_options_with_comment(typescript, jsx, comment, source_text); + } +} + +fn update_options_with_comment( + typescript: &mut TypeScriptOptions, + jsx: &mut JsxOptions, + comment: &Comment, + source_text: &str, +) { + let mut comment_str = comment.content_span().source_text(source_text); + + while let Some((keyword, value, remainder)) = find_jsx_pragma(comment_str) { + match keyword { + // @jsx + PragmaType::Jsx => { + // Don't set React option unless React transform is enabled + // otherwise can cause error in `ReactJsx::new` + if jsx.jsx_plugin || jsx.development { + jsx.pragma = Some(value.to_string()); + } + typescript.jsx_pragma = Cow::Owned(value.to_string()); + } + // @jsxRuntime + PragmaType::JsxRuntime => match value { + "classic" => jsx.runtime = JsxRuntime::Classic, + "automatic" => jsx.runtime = JsxRuntime::Automatic, + _ => {} + }, + // @jsxImportSource + PragmaType::JsxImportSource => { + jsx.import_source = Some(value.to_string()); + } + // @jsxFrag + PragmaType::JsxFrag => { + // Don't set React option unless React transform is enabled + // otherwise can cause error in `ReactJsx::new` + if jsx.jsx_plugin || jsx.development { + jsx.pragma_frag = Some(value.to_string()); + } + typescript.jsx_pragma_frag = Cow::Owned(value.to_string()); + } + } + + // Search again for another pragma + comment_str = remainder; + } +} + +/// Type of JSX pragma directive. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum PragmaType { + Jsx, + JsxRuntime, + JsxImportSource, + JsxFrag, +} + +/// Search comment for a JSX pragma. +/// +/// If found, returns: +/// +/// * `PragmaType` representing the type of the pragma. +/// * Value following `@jsx` / `@jsxRuntime` / etc. +/// * The remainder of the comment, to search again for another pragma. +/// +/// If no pragma found, returns `None`. +fn find_jsx_pragma(mut comment_str: &str) -> Option<(PragmaType, &str, &str)> { + let pragma_type; + loop { + // Search for `@`. + // Note: Using `memchr::memmem::Finder` to search for `@jsx` is slower than only using `memchr` + // to find `@` characters, and then checking if `@` is followed by `jsx` separately. + let at_sign_index = memchr(b'@', comment_str.as_bytes())?; + + // Check `@` is start of `@jsx`. + // Note: Checking 4 bytes including leading `@` is faster than checking the 3 bytes after `@`, + // because 4 bytes is a `u32`. + let next4 = comment_str.as_bytes().get(at_sign_index..at_sign_index + 4)?; + if next4 != b"@jsx" { + // Not `@jsx`. Trim off up to and including `@` and search again. + // SAFETY: Byte at `at_sign_index` is `@`, so `at_sign_index + 1` is either within string + // or end of string, and on a UTF-8 char boundary. + comment_str = unsafe { comment_str.get_unchecked(at_sign_index + 1..) }; + continue; + } + + // Trim `@jsx` and everything before it from start of `comment_str`. + // SAFETY: 4 bytes starting at `at_sign_index` are `@jsx`, so `at_sign_index + 4` is within string + // or end of string, and must be on a UTF-8 character boundary. + comment_str = unsafe { comment_str.get_unchecked(at_sign_index + 4..) }; + + // Get rest of keyword e.g. `Runtime` in `@jsxRuntime` + let space_index = comment_str.as_bytes().iter().position(|&b| matches!(b, b' ' | b'\t'))?; + // SAFETY: Byte at `space_index` is ASCII, so `space_index` is in bounds and on a UTF-8 char boundary + let keyword_str = unsafe { comment_str.get_unchecked(..space_index) }; + // SAFETY: Byte at `space_index` is ASCII, so `space_index + 1` is in bounds and on a UTF-8 char boundary + comment_str = unsafe { comment_str.get_unchecked(space_index + 1..) }; + + pragma_type = match keyword_str { + "" => PragmaType::Jsx, + "Runtime" => PragmaType::JsxRuntime, + "ImportSource" => PragmaType::JsxImportSource, + "Frag" => PragmaType::JsxFrag, + _ => { + // Unrecognised pragma - search for another + continue; + } + }; + break; + } + + // Consume any further spaces / tabs after keyword + loop { + let next_byte = *comment_str.as_bytes().first()?; + if !matches!(next_byte, b' ' | b'\t') { + break; + } + // SAFETY: First byte of string is ASCII, so trimming it off must leave a valid UTF-8 string + comment_str = unsafe { comment_str.get_unchecked(1..) }; + } + + // Get value + let space_index = comment_str.as_bytes().iter().position(|&b| is_ascii_whitespace(b)); + let value; + if let Some(space_index) = space_index { + // SAFETY: Byte at `space_index` is ASCII, so `space_index` is in bounds and on a UTF-8 char boundary + value = unsafe { comment_str.get_unchecked(..space_index) }; + // SAFETY: Byte at `space_index` is ASCII, so `space_index + 1` is in bounds and on a UTF-8 char boundary + comment_str = unsafe { comment_str.get_unchecked(space_index + 1..) }; + } else { + value = comment_str; + comment_str = ""; + } + + if value.is_empty() { None } else { Some((pragma_type, value, comment_str)) } +} + +/// Test if a byte is ASCII whitespace, using the same group of ASCII chars that `std::str::trim_start` uses. +/// These the are ASCII chars which `char::is_whitespace` returns `true` for. +/// Note: Slightly different from `u8::is_ascii_whitespace`, which does not include VT. +/// +#[inline] +fn is_ascii_whitespace(byte: u8) -> bool { + const VT: u8 = 0x0B; + const FF: u8 = 0x0C; + matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | VT | FF) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_find_jsx_pragma() { + let cases: &[(&str, &[(PragmaType, &str)])] = &[ + // No valid pragmas + ("", &[]), + ("blah blah blah", &[]), + ("@jsxDonkey abc", &[]), + // Single pragma + ("@jsx h", &[(PragmaType::Jsx, "h")]), + ("@jsx React.createDumpling", &[(PragmaType::Jsx, "React.createDumpling")]), + ("@jsxRuntime classic", &[(PragmaType::JsxRuntime, "classic")]), + ("@jsxImportSource preact", &[(PragmaType::JsxImportSource, "preact")]), + ("@jsxFrag Fraggy", &[(PragmaType::JsxFrag, "Fraggy")]), + // Multiple pragmas + ( + "@jsx h @jsxRuntime classic", + &[(PragmaType::Jsx, "h"), (PragmaType::JsxRuntime, "classic")], + ), + ( + "* @jsx h\n * @jsxRuntime classic\n *", + &[(PragmaType::Jsx, "h"), (PragmaType::JsxRuntime, "classic")], + ), + ( + "@jsx h @jsxRuntime classic @jsxImportSource importer-a-go-go @jsxFrag F", + &[ + (PragmaType::Jsx, "h"), + (PragmaType::JsxRuntime, "classic"), + (PragmaType::JsxImportSource, "importer-a-go-go"), + (PragmaType::JsxFrag, "F"), + ], + ), + ( + "* @jsx h\n * @jsxRuntime classic\n * @jsxImportSource importer-a-go-go\n * @jsxFrag F\n *", + &[ + (PragmaType::Jsx, "h"), + (PragmaType::JsxRuntime, "classic"), + (PragmaType::JsxImportSource, "importer-a-go-go"), + (PragmaType::JsxFrag, "F"), + ], + ), + // Text in between pragmas + ( + "@jsx h blah blah @jsxRuntime classic", + &[(PragmaType::Jsx, "h"), (PragmaType::JsxRuntime, "classic")], + ), + ( + "blah blah\n * @jsx h \n * blah blah\n * @jsxRuntime classic \n * blah blah", + &[(PragmaType::Jsx, "h"), (PragmaType::JsxRuntime, "classic")], + ), + // Pragma without value + ("@jsx", &[]), + ("@jsxRuntime", &[]), + // Other invalid pragmas surrounding valid one + ("@moon @jsx h @moon", &[(PragmaType::Jsx, "h")]), + ("@jsxX @jsx h @jsxX", &[(PragmaType::Jsx, "h")]), + ("@jsxMoon @jsx h @jsxMoon", &[(PragmaType::Jsx, "h")]), + ("@jsx @jsx h", &[(PragmaType::Jsx, "@jsx")]), + // Multiple `@` signs + ("@@@@@jsx h", &[(PragmaType::Jsx, "h")]), + ]; + + let prefixes = ["", " ", "\n\n", "*\n* "]; + let postfixes = ["", " ", "\n\n", "\n*"]; + + for (comment_str, expected) in cases { + for prefix in prefixes { + for postfix in postfixes { + let comment_str = format!("{prefix}{comment_str}{postfix}"); + let mut comment_str = comment_str.as_str(); + let mut pragmas = vec![]; + while let Some((pragma_type, value, remaining)) = find_jsx_pragma(comment_str) { + pragmas.push((pragma_type, value)); + comment_str = remaining; + } + assert_eq!(&pragmas, expected); + } + } + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/diagnostics.rs b/crates/swc_ecma_transformer/oxc/jsx/diagnostics.rs new file mode 100644 index 000000000000..cfc896ca8d3c --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/diagnostics.rs @@ -0,0 +1,32 @@ +use oxc_diagnostics::OxcDiagnostic; +use oxc_span::Span; + +pub fn pragma_and_pragma_frag_cannot_be_set() -> OxcDiagnostic { + OxcDiagnostic::warn("pragma and pragmaFrag cannot be set when runtime is automatic.") + .with_help("Remove `pragma` and `pragmaFrag` options.") +} + +pub fn invalid_pragma() -> OxcDiagnostic { + OxcDiagnostic::warn("pragma and pragmaFrag must be of the form `foo` or `foo.bar`.") + .with_help("Fix `pragma` and `pragmaFrag` options.") +} + +pub fn import_source_cannot_be_set() -> OxcDiagnostic { + OxcDiagnostic::warn("importSource cannot be set when runtime is classic.") + .with_help("Remove `importSource` option.") +} + +pub fn invalid_import_source() -> OxcDiagnostic { + OxcDiagnostic::warn("importSource cannot be an empty string or longer than u32::MAX bytes") + .with_help("Fix `importSource` option.") +} + +pub fn namespace_does_not_support(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Namespace tags are not supported by default. React's JSX doesn't support namespace tags. You can set `throwIfNamespace: false` to bypass this warning.") + .with_label(span) +} + +pub fn valueless_key(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Please provide an explicit key value. Using \"key\" as a shorthand for \"key={true}\" is not allowed.") + .with_label(span) +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/display_name.rs b/crates/swc_ecma_transformer/oxc/jsx/display_name.rs new file mode 100644 index 000000000000..439b091271cc --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/display_name.rs @@ -0,0 +1,186 @@ +//! React Display Name +//! +//! Adds `displayName` property to `React.createClass` calls. +//! +//! > This plugin is included in `preset-react`. +//! +//! ## Example +//! +//! Input: +//! ```js +//! // some_filename.jsx +//! var foo = React.createClass({}); // React <= 15 +//! bar = createReactClass({}); // React 16+ +//! +//! var obj = { prop: React.createClass({}) }; +//! obj.prop2 = React.createClass({}); +//! obj["prop 3"] = React.createClass({}); +//! export default React.createClass({}); +//! ``` +//! +//! Output: +//! ```js +//! var foo = React.createClass({ displayName: "foo" }); +//! bar = createReactClass({ displayName: "bar" }); +//! +//! var obj = { prop: React.createClass({ displayName: "prop" }) }; +//! obj.prop2 = React.createClass({ displayName: "prop2" }); +//! obj["prop 3"] = React.createClass({ displayName: "prop 3" }); +//! export default React.createClass({ displayName: "some_filename" }); +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-react-display-name](https://babeljs.io/docs/babel-plugin-transform-react-display-name). +//! +//! Babel does not get the display name for this example: +//! +//! ```js +//! obj["prop 3"] = React.createClass({}); +//! ``` +//! +//! This implementation does, which is a divergence from Babel, but probably an improvement. +//! +//! ## References: +//! +//! * Babel plugin implementation: + +use oxc_ast::ast::*; +use oxc_span::{Atom, SPAN}; +use oxc_traverse::{Ancestor, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct ReactDisplayName<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> ReactDisplayName<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ReactDisplayName<'a, '_> { + fn enter_call_expression( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Some(obj_expr) = Self::get_object_from_create_class(call_expr) else { + return; + }; + + let mut ancestors = ctx.ancestors(); + let name = loop { + let Some(ancestor) = ancestors.next() else { + return; + }; + + match ancestor { + // `foo = React.createClass({})` + Ancestor::AssignmentExpressionRight(assign_expr) => match assign_expr.left() { + AssignmentTarget::AssignmentTargetIdentifier(ident) => { + break ident.name; + } + AssignmentTarget::StaticMemberExpression(expr) => { + break expr.property.name; + } + // Babel does not handle computed member expressions e.g. `foo["bar"]`, + // so we diverge from Babel here, but that's probably an improvement + AssignmentTarget::ComputedMemberExpression(expr) => { + if let Some(name) = expr.static_property_name() { + break name; + } + return; + } + _ => return, + }, + // `let foo = React.createClass({})` + Ancestor::VariableDeclaratorInit(declarator) => { + if let BindingPatternKind::BindingIdentifier(ident) = &declarator.id().kind { + break ident.name; + } + return; + } + // `{foo: React.createClass({})}` + Ancestor::ObjectPropertyValue(prop) => { + // Babel only handles static identifiers e.g. `{foo: React.createClass({})}`, + // whereas we also handle e.g. `{"foo-bar": React.createClass({})}`, + // so we diverge from Babel here, but that's probably an improvement + if let Some(name) = prop.key().static_name() { + break ctx.ast.atom(&name); + } + return; + } + // `export default React.createClass({})` + // Uses the current file name as the display name. + Ancestor::ExportDefaultDeclarationDeclaration(_) => { + break ctx.ast.atom(&self.ctx.filename); + } + // Stop crawling up when hit a statement + _ if ancestor.is_parent_of_statement() => return, + _ => {} + } + }; + + Self::add_display_name(obj_expr, name, ctx); + } +} + +impl<'a> ReactDisplayName<'a, '_> { + /// Get the object from `React.createClass({})` or `createReactClass({})` + fn get_object_from_create_class<'b>( + call_expr: &'b mut CallExpression<'a>, + ) -> Option<&'b mut ObjectExpression<'a>> { + if match &call_expr.callee { + callee @ match_member_expression!(Expression) => { + !callee.to_member_expression().is_specific_member_access("React", "createClass") + } + Expression::Identifier(ident) => ident.name != "createReactClass", + _ => true, + } { + return None; + } + // Only 1 argument being the object expression. + if call_expr.arguments.len() != 1 { + return None; + } + let arg = call_expr.arguments.get_mut(0)?; + match arg { + Argument::ObjectExpression(obj_expr) => Some(obj_expr), + _ => None, + } + } + + /// Add key value `displayName: name` to the `React.createClass` object. + fn add_display_name( + obj_expr: &mut ObjectExpression<'a>, + name: Atom<'a>, + ctx: &TraverseCtx<'a>, + ) { + const DISPLAY_NAME: &str = "displayName"; + // Not safe with existing display name. + let not_safe = obj_expr.properties.iter().any(|prop| { + matches!(prop, ObjectPropertyKind::ObjectProperty(p) if p.key.static_name().is_some_and(|name| name == DISPLAY_NAME)) + }); + if not_safe { + return; + } + obj_expr.properties.insert( + 0, + ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + ctx.ast.property_key_static_identifier(SPAN, DISPLAY_NAME), + ctx.ast.expression_string_literal(SPAN, name, None), + false, + false, + false, + ), + ); + } +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/jsx_impl.rs b/crates/swc_ecma_transformer/oxc/jsx/jsx_impl.rs new file mode 100644 index 000000000000..6e519a93bddf --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/jsx_impl.rs @@ -0,0 +1,1388 @@ +//! React JSX +//! +//! This plugin transforms React JSX to JS. +//! +//! > This plugin is included in `preset-react`. +//! +//! Has two modes which create different output: +//! 1. Automatic +//! 2. Classic +//! +//! And also prod/dev modes: +//! 1. Production +//! 2. Development +//! +//! ## Example +//! +//! ### Automatic +//! +//! Input: +//! ```js +//!
foo
; +//! foo; +//! <>foo; +//! ``` +//! +//! Output: +//! ```js +//! // Production mode +//! import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; +//! _jsx("div", { children: "foo" }); +//! _jsx(Bar, { children: "foo" }); +//! _jsx(_Fragment, { children: "foo" }); +//! ``` +//! +//! ```js +//! // Development mode +//! var _jsxFileName = "/test.js"; +//! import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime"; +//! _jsxDEV( +//! "div", { children: "foo" }, void 0, false, +//! { fileName: _jsxFileName, lineNumber: 1, columnNumber: 1 }, +//! this +//! ); +//! _jsxDEV( +//! Bar, { children: "foo" }, void 0, false, +//! { fileName: _jsxFileName, lineNumber: 2, columnNumber: 1 }, +//! this +//! ); +//! _jsxDEV(_Fragment, { children: "foo" }, void 0, false); +//! ``` +//! +//! ### Classic +//! +//! Input: +//! ```js +//!
foo
; +//! foo; +//! <>foo; +//! ``` +//! +//! Output: +//! ```js +//! // Production mode +//! React.createElement("div", null, "foo"); +//! React.createElement(Bar, null, "foo"); +//! React.createElement(React.Fragment, null, "foo"); +//! ``` +//! +//! ```js +//! // Development mode +//! var _jsxFileName = "/test.js"; +//! React.createElement("div", { +//! __self: this, +//! __source: { fileName: _jsxFileName, lineNumber: 1, columnNumber: 1 } +//! }, "foo"); +//! React.createElement(Bar, { +//! __self: this, +//! __source: { fileName: _jsxFileName, lineNumber: 2, columnNumber: 1 } +//! }, "foo"); +//! React.createElement(React.Fragment, null, "foo"); +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-react-jsx](https://babeljs.io/docs/babel-plugin-transform-react-jsx). +//! +//! ## References: +//! +//! * Babel plugin implementation: + +use oxc_allocator::{ + Box as ArenaBox, StringBuilder as ArenaStringBuilder, TakeIn, Vec as ArenaVec, +}; +use oxc_ast::{AstBuilder, NONE, ast::*}; +use oxc_ecmascript::PropName; +use oxc_span::{Atom, SPAN, Span}; +use oxc_syntax::{ + identifier::{is_line_terminator, is_white_space_single_line}, + reference::ReferenceFlags, + symbol::SymbolFlags, + xml_entities::XML_ENTITIES, +}; +use oxc_traverse::{BoundIdentifier, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + es2018::{ObjectRestSpread, ObjectRestSpreadOptions}, + state::TransformState, +}; + +use super::{ + diagnostics, + jsx_self::JsxSelf, + jsx_source::JsxSource, + options::{JsxOptions, JsxRuntime}, +}; + +pub struct JsxImpl<'a, 'ctx> { + pure: bool, + options: JsxOptions, + object_rest_spread_options: Option, + + ctx: &'ctx TransformCtx<'a>, + + pub(super) jsx_self: JsxSelf<'a, 'ctx>, + pub(super) jsx_source: JsxSource<'a, 'ctx>, + + // States + bindings: Bindings<'a, 'ctx>, +} + +/// Bindings for different import options +enum Bindings<'a, 'ctx> { + Classic(ClassicBindings<'a>), + AutomaticScript(AutomaticScriptBindings<'a, 'ctx>), + AutomaticModule(AutomaticModuleBindings<'a, 'ctx>), +} + +impl Bindings<'_, '_> { + #[inline] + fn is_classic(&self) -> bool { + matches!(self, Self::Classic(_)) + } +} + +struct ClassicBindings<'a> { + pragma: Pragma<'a>, + pragma_frag: Pragma<'a>, +} + +struct AutomaticScriptBindings<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + jsx_runtime_importer: Atom<'a>, + react_importer_len: u32, + require_create_element: Option>, + require_jsx: Option>, + is_development: bool, +} + +impl<'a, 'ctx> AutomaticScriptBindings<'a, 'ctx> { + fn new( + ctx: &'ctx TransformCtx<'a>, + jsx_runtime_importer: Atom<'a>, + react_importer_len: u32, + is_development: bool, + ) -> Self { + Self { + ctx, + jsx_runtime_importer, + react_importer_len, + require_create_element: None, + require_jsx: None, + is_development, + } + } + + fn require_create_element(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> { + if self.require_create_element.is_none() { + let source = + get_import_source(self.jsx_runtime_importer.as_str(), self.react_importer_len); + // We have to insert this `require` above `require("react/jsx-runtime")` + // just to pass one of Babel's tests, but the order doesn't actually matter. + // TODO(improve-on-babel): Remove this once we don't need our output to match Babel exactly. + let id = self.add_require_statement("react", source, true, ctx); + self.require_create_element = Some(id); + } + self.require_create_element.as_ref().unwrap().create_read_reference(ctx) + } + + fn require_jsx(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> { + if self.require_jsx.is_none() { + let var_name = + if self.is_development { "reactJsxDevRuntime" } else { "reactJsxRuntime" }; + let id = self.add_require_statement(var_name, self.jsx_runtime_importer, false, ctx); + self.require_jsx = Some(id); + } + self.require_jsx.as_ref().unwrap().create_read_reference(ctx) + } + + fn add_require_statement( + &self, + variable_name: &str, + source: Atom<'a>, + front: bool, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + let binding = + ctx.generate_uid_in_root_scope(variable_name, SymbolFlags::FunctionScopedVariable); + self.ctx.module_imports.add_default_import(source, binding.clone(), front); + binding + } +} + +struct AutomaticModuleBindings<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + jsx_runtime_importer: Atom<'a>, + react_importer_len: u32, + import_create_element: Option>, + import_fragment: Option>, + import_jsx: Option>, + import_jsxs: Option>, + is_development: bool, +} + +impl<'a, 'ctx> AutomaticModuleBindings<'a, 'ctx> { + fn new( + ctx: &'ctx TransformCtx<'a>, + jsx_runtime_importer: Atom<'a>, + react_importer_len: u32, + is_development: bool, + ) -> Self { + Self { + ctx, + jsx_runtime_importer, + react_importer_len, + import_create_element: None, + import_fragment: None, + import_jsx: None, + import_jsxs: None, + is_development, + } + } + + fn import_create_element(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> { + if self.import_create_element.is_none() { + let source = + get_import_source(self.jsx_runtime_importer.as_str(), self.react_importer_len); + let id = self.add_import_statement("createElement", source, ctx); + self.import_create_element = Some(id); + } + self.import_create_element.as_ref().unwrap().create_read_reference(ctx) + } + + fn import_fragment(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> { + if self.import_fragment.is_none() { + self.import_fragment = Some(self.add_jsx_import_statement("Fragment", ctx)); + } + self.import_fragment.as_ref().unwrap().create_read_reference(ctx) + } + + fn import_jsx(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> { + if self.import_jsx.is_none() { + if self.is_development { + self.add_import_jsx_dev(ctx); + } else { + self.import_jsx = Some(self.add_jsx_import_statement("jsx", ctx)); + } + } + self.import_jsx.as_ref().unwrap().create_read_reference(ctx) + } + + fn import_jsxs(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> { + if self.import_jsxs.is_none() { + if self.is_development { + self.add_import_jsx_dev(ctx); + } else { + self.import_jsxs = Some(self.add_jsx_import_statement("jsxs", ctx)); + } + } + self.import_jsxs.as_ref().unwrap().create_read_reference(ctx) + } + + // Inline so that compiler can see in `import_jsx` and `import_jsxs` that fields + // are always `Some` after calling this function, and can elide the `unwrap()`s + #[inline] + fn add_import_jsx_dev(&mut self, ctx: &mut TraverseCtx<'a>) { + let id = self.add_jsx_import_statement("jsxDEV", ctx); + self.import_jsx = Some(id.clone()); + self.import_jsxs = Some(id); + } + + fn add_jsx_import_statement( + &self, + name: &'static str, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + self.add_import_statement(name, self.jsx_runtime_importer, ctx) + } + + fn add_import_statement( + &self, + name: &'static str, + source: Atom<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + let binding = ctx.generate_uid_in_root_scope(name, SymbolFlags::Import); + self.ctx.module_imports.add_named_import(source, Atom::from(name), binding.clone(), false); + binding + } +} + +#[inline] +fn get_import_source(jsx_runtime_importer: &str, react_importer_len: u32) -> Atom<'_> { + Atom::from(&jsx_runtime_importer[..react_importer_len as usize]) +} + +/// Pragma used in classic mode. +/// +/// `Double` is first as it's most common. +enum Pragma<'a> { + /// `React.createElement` + Double(Atom<'a>, Atom<'a>), + /// `createElement` + Single(Atom<'a>), + /// `foo.bar.qux` + Multiple(Vec>), + /// `this`, `this.foo`, `this.foo.bar.qux` + This(Vec>), + /// `import.meta`, `import.meta.foo`, `import.meta.foo.bar.qux` + ImportMeta(Vec>), +} + +impl<'a> Pragma<'a> { + /// Parse `options.pragma` or `options.pragma_frag`. + /// + /// If provided option is invalid, raise an error and use default. + fn parse( + pragma: Option<&str>, + default_property_name: &'static str, + ast: AstBuilder<'a>, + ctx: &TransformCtx<'a>, + ) -> Self { + if let Some(pragma) = pragma { + if pragma.is_empty() { + ctx.error(diagnostics::invalid_pragma()); + } else { + return Self::parse_impl(pragma, ast); + } + } + + Self::Double(Atom::from("React"), Atom::from(default_property_name)) + } + + fn parse_impl(pragma: &str, ast: AstBuilder<'a>) -> Self { + let strs_to_atoms = |parts: &[&str]| parts.iter().map(|part| ast.atom(part)).collect(); + + let parts = pragma.split('.').collect::>(); + match &parts[..] { + [] => unreachable!(), + ["this", rest @ ..] => Self::This(strs_to_atoms(rest)), + ["import", "meta", rest @ ..] => Self::ImportMeta(strs_to_atoms(rest)), + [first, second] => Self::Double(ast.atom(first), ast.atom(second)), + [only] => Self::Single(ast.atom(only)), + parts => Self::Multiple(strs_to_atoms(parts)), + } + } + + fn create_expression(&self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + let (object, parts) = match self { + Self::Double(first, second) => { + let object = get_read_identifier_reference(SPAN, *first, ctx); + return Expression::from(ctx.ast.member_expression_static( + SPAN, + object, + ctx.ast.identifier_name(SPAN, *second), + false, + )); + } + Self::Single(single) => { + return get_read_identifier_reference(SPAN, *single, ctx); + } + Self::Multiple(parts) => { + let mut parts = parts.iter(); + let first = *parts.next().unwrap(); + let object = get_read_identifier_reference(SPAN, first, ctx); + (object, parts) + } + Self::This(parts) => { + let object = ctx.ast.expression_this(SPAN); + (object, parts.iter()) + } + Self::ImportMeta(parts) => { + let object = ctx.ast.expression_meta_property( + SPAN, + ctx.ast.identifier_name(SPAN, Atom::from("import")), + ctx.ast.identifier_name(SPAN, Atom::from("meta")), + ); + (object, parts.iter()) + } + }; + + let mut expr = object; + for &item in parts { + let name = ctx.ast.identifier_name(SPAN, item); + expr = ctx.ast.member_expression_static(SPAN, expr, name, false).into(); + } + expr + } +} + +impl<'a, 'ctx> JsxImpl<'a, 'ctx> { + pub fn new( + options: JsxOptions, + object_rest_spread_options: Option, + ast: AstBuilder<'a>, + ctx: &'ctx TransformCtx<'a>, + ) -> Self { + // Only add `pure` when `pure` is explicitly set to `true` or all JSX options are default. + let pure = options.pure || (options.import_source.is_none() && options.pragma.is_none()); + let bindings = match options.runtime { + JsxRuntime::Classic => { + if options.import_source.is_some() { + ctx.error(diagnostics::import_source_cannot_be_set()); + } + let pragma = Pragma::parse(options.pragma.as_deref(), "createElement", ast, ctx); + let pragma_frag = + Pragma::parse(options.pragma_frag.as_deref(), "Fragment", ast, ctx); + Bindings::Classic(ClassicBindings { pragma, pragma_frag }) + } + JsxRuntime::Automatic => { + if options.pragma.is_some() || options.pragma_frag.is_some() { + ctx.error(diagnostics::pragma_and_pragma_frag_cannot_be_set()); + } + + let is_development = options.development; + #[expect(clippy::single_match_else, clippy::cast_possible_truncation)] + let (jsx_runtime_importer, source_len) = match options.import_source.as_ref() { + Some(import_source) => { + let mut import_source = &**import_source; + let source_len = match u32::try_from(import_source.len()) { + Ok(0) | Err(_) => { + ctx.error(diagnostics::invalid_import_source()); + import_source = "react"; + import_source.len() as u32 + } + Ok(source_len) => source_len, + }; + let jsx_runtime_importer = ast.atom(&format!( + "{}/jsx-{}runtime", + import_source, + if is_development { "dev-" } else { "" } + )); + (jsx_runtime_importer, source_len) + } + None => { + let jsx_runtime_importer = if is_development { + Atom::from("react/jsx-dev-runtime") + } else { + Atom::from("react/jsx-runtime") + }; + (jsx_runtime_importer, "react".len() as u32) + } + }; + + if ctx.source_type.is_script() { + Bindings::AutomaticScript(AutomaticScriptBindings::new( + ctx, + jsx_runtime_importer, + source_len, + is_development, + )) + } else { + Bindings::AutomaticModule(AutomaticModuleBindings::new( + ctx, + jsx_runtime_importer, + source_len, + is_development, + )) + } + } + }; + + Self { + pure, + options, + object_rest_spread_options, + ctx, + jsx_self: JsxSelf::new(ctx), + jsx_source: JsxSource::new(ctx), + bindings, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for JsxImpl<'a, '_> { + fn exit_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.insert_filename_var_statement(ctx); + } + + #[inline] + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if !matches!(expr, Expression::JSXElement(_) | Expression::JSXFragment(_)) { + return; + } + *expr = match expr.take_in(ctx.ast) { + Expression::JSXElement(e) => self.transform_jsx_element(e, ctx), + Expression::JSXFragment(e) => self.transform_jsx(e.span, None, e.unbox().children, ctx), + _ => unreachable!(), + }; + } +} + +impl<'a> JsxImpl<'a, '_> { + fn is_script(&self) -> bool { + self.ctx.source_type.is_script() + } + + fn insert_filename_var_statement(&self, ctx: &TraverseCtx<'a>) { + let Some(declarator) = self.jsx_source.get_filename_var_declarator(ctx) else { return }; + + // If is a module, add filename statements before `import`s. If script, then after `require`s. + // This is the same behavior as Babel. + // If in classic mode, then there are no import statements, so it doesn't matter either way. + // TODO(improve-on-babel): Simplify this once we don't need to follow Babel exactly. + if self.bindings.is_classic() || !self.is_script() { + // Insert before imports - add to `top_level_statements` immediately + let stmt = Statement::VariableDeclaration(ctx.ast.alloc_variable_declaration( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(declarator), + false, + )); + self.ctx.top_level_statements.insert_statement(stmt); + } else { + // Insert after imports - add to `var_declarations`, which are inserted after `require` statements + self.ctx.var_declarations.insert_var_declarator(declarator, ctx); + } + } + + fn transform_jsx_element( + &mut self, + element: ArenaBox<'a, JSXElement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let JSXElement { span, opening_element, closing_element, children } = element.unbox(); + Self::delete_reference_for_closing_element(closing_element.as_deref(), ctx); + self.transform_jsx(span, Some(opening_element), children, ctx) + } + + fn transform_jsx( + &mut self, + span: Span, + opening_element: Option>>, + children: ArenaVec>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let has_key_after_props_spread = + opening_element.as_ref().is_some_and(|e| Self::has_key_after_props_spread(e)); + // If has_key_after_props_spread is true, we need to fallback to `createElement` same behavior as classic runtime + let is_classic = self.bindings.is_classic() || has_key_after_props_spread; + let is_automatic = !is_classic; + let is_development = self.options.development; + let is_element = opening_element.is_some(); + + // The maximum capacity length of arguments allowed. + let capacity = if is_classic { 3 + children.len() } else { 6 }; + let mut arguments = ctx.ast.vec_with_capacity(capacity); + + // The key prop in `
` + let mut key_prop = None; + + // The object properties for the second argument of `React.createElement` + let mut properties = ctx.ast.vec(); + let (element_name, attributes) = opening_element + .map(|e| { + let e = e.unbox(); + (Some(e.name), Some(e.attributes)) + }) + .unwrap_or_default(); + + if let Some(attributes) = attributes { + let attributes_len = attributes.len(); + for attribute in attributes { + match attribute { + JSXAttributeItem::Attribute(attr) => { + let JSXAttribute { span, name, value } = attr.unbox(); + match &name { + JSXAttributeName::Identifier(ident) + if self.options.development + && self.options.jsx_self_plugin + && ident.name == "__self" => + { + self.jsx_self.report_error(ident.span); + } + JSXAttributeName::Identifier(ident) + if self.options.development + && self.options.jsx_source_plugin + && ident.name == "__source" => + { + self.jsx_source.report_error(ident.span); + } + JSXAttributeName::Identifier(ident) if ident.name == "key" => { + if value.is_none() { + self.ctx.error(diagnostics::valueless_key(ident.span)); + } else if is_automatic { + // In automatic mode, extract the key before spread prop, + // and add it to the third argument later. + key_prop = value; + continue; + } + } + _ => {} + } + + // Add attribute to prop object + let kind = PropertyKind::Init; + let key = Self::get_attribute_name(name, ctx); + let value = self.transform_jsx_attribute_value(value, ctx); + let object_property = ctx.ast.object_property_kind_object_property( + span, kind, key, value, false, false, false, + ); + properties.push(object_property); + } + // optimize `{...prop}` to `prop` in static mode + JSXAttributeItem::SpreadAttribute(spread) => { + let JSXSpreadAttribute { argument, span } = spread.unbox(); + if is_classic + && attributes_len == 1 + // Don't optimize when dev plugins are enabled - spread must be preserved + // to merge with injected `__self` and `__source` props + && !(self.options.jsx_self_plugin || self.options.jsx_source_plugin) + { + // deopt if spreading an object with `__proto__` key + if !matches!(&argument, Expression::ObjectExpression(o) if has_proto(o)) + { + arguments.push(Argument::from(argument)); + continue; + } + } + + // Add attribute to prop object + match argument { + Expression::ObjectExpression(expr) if !has_proto(&expr) => { + properties.extend(expr.unbox().properties); + } + argument => { + let object_property = + ctx.ast.object_property_kind_spread_property(span, argument); + properties.push(object_property); + } + } + } + } + } + } + + let mut need_jsxs = false; + + // Append children to object properties in automatic mode + if is_automatic { + let mut children = ctx.ast.vec_from_iter( + children + .into_iter() + .filter_map(|child| self.transform_jsx_child_automatic(child, ctx)), + ); + let children_len = children.len(); + if children_len != 0 { + let value = if children_len == 1 { + children.pop().unwrap() + } else { + need_jsxs = true; + let elements = children.into_iter().map(ArrayExpressionElement::from); + let elements = ctx.ast.vec_from_iter(elements); + ctx.ast.expression_array(SPAN, elements) + }; + let children = ctx.ast.property_key_static_identifier(SPAN, "children"); + let kind = PropertyKind::Init; + let property = ctx.ast.object_property_kind_object_property( + SPAN, kind, children, value, false, false, false, + ); + properties.push(property); + } + + // If runtime is automatic that means we always to add `{ .. }` as the second argument even if it's empty + let mut object_expression = ctx.ast.expression_object(SPAN, properties); + if let Some(options) = self.object_rest_spread_options { + ObjectRestSpread::transform_object_expression( + options, + &mut object_expression, + self.ctx, + ctx, + ); + } + arguments.push(Argument::from(object_expression)); + + // Only jsx and jsxDev will have more than 2 arguments + // key + if key_prop.is_some() { + arguments.push(Argument::from(self.transform_jsx_attribute_value(key_prop, ctx))); + } else if is_development { + arguments.push(Argument::from(ctx.ast.void_0(SPAN))); + } + + // isStaticChildren + if is_development { + arguments.push(Argument::from( + ctx.ast.expression_boolean_literal(SPAN, children_len > 1), + )); + } + + // Fragment doesn't have source and self + if is_element { + // { __source: { fileName, lineNumber, columnNumber } } + if self.options.jsx_source_plugin { + let (line, column) = self.jsx_source.get_line_column(span.start); + let expr = self.jsx_source.get_source_object(line, column, ctx); + arguments.push(Argument::from(expr)); + } + + // this + if self.options.jsx_self_plugin && JsxSelf::can_add_self_attribute(ctx) { + arguments.push(Argument::from(ctx.ast.expression_this(SPAN))); + } + } + } else { + // React.createElement's second argument + if is_element { + if self.options.jsx_self_plugin && JsxSelf::can_add_self_attribute(ctx) { + properties.push(JsxSelf::get_object_property_kind_for_jsx_plugin(ctx)); + } + + if self.options.jsx_source_plugin { + let (line, column) = self.jsx_source.get_line_column(span.start); + properties.push( + self.jsx_source.get_object_property_kind_for_jsx_plugin(line, column, ctx), + ); + } + } + + if !properties.is_empty() { + let mut object_expression = ctx.ast.expression_object(SPAN, properties); + if let Some(options) = self.object_rest_spread_options { + ObjectRestSpread::transform_object_expression( + options, + &mut object_expression, + self.ctx, + ctx, + ); + } + arguments.push(Argument::from(object_expression)); + } else if arguments.is_empty() { + // If not and second argument doesn't exist, we should add `null` as the second argument + let null_expr = ctx.ast.expression_null_literal(SPAN); + arguments.push(Argument::from(null_expr)); + } + + // React.createElement(type, arguments, ...children) + // ^^^^^^^^^^^ + arguments.extend( + children + .into_iter() + .filter_map(|child| self.transform_jsx_child_classic(child, ctx)), + ); + } + + // It would be better to push to `arguments` earlier, rather than use `insert`. + // But we have to do it here to replicate the same import order as Babel, in order to pass + // Babel's conformance tests. + // TODO(improve-on-babel): Change this if we can handle differing output in tests. + let argument_expr = if let Some(element_name) = element_name { + self.transform_element_name(element_name, ctx) + } else { + self.get_fragment(ctx) + }; + arguments.insert(0, Argument::from(argument_expr)); + debug_assert!(arguments.len() <= capacity); + + let callee = self.get_create_element(has_key_after_props_spread, need_jsxs, ctx); + ctx.ast.expression_call_with_pure(span, callee, NONE, arguments, false, self.pure) + } + + fn transform_element_name( + &self, + name: JSXElementName<'a>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + match name { + JSXElementName::Identifier(ident) => { + ctx.ast.expression_string_literal(ident.span, ident.name, None) + } + JSXElementName::IdentifierReference(ident) => Expression::Identifier(ident), + JSXElementName::MemberExpression(member_expr) => { + Self::transform_jsx_member_expression(member_expr, ctx) + } + JSXElementName::NamespacedName(namespaced) => { + if self.options.throw_if_namespace { + self.ctx.error(diagnostics::namespace_does_not_support(namespaced.span)); + } + let namespace_name = ctx.ast.atom_from_strs_array([ + &namespaced.namespace.name, + ":", + &namespaced.name.name, + ]); + ctx.ast.expression_string_literal(namespaced.span, namespace_name, None) + } + JSXElementName::ThisExpression(expr) => ctx.ast.expression_this(expr.span), + } + } + + fn get_fragment(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + match &mut self.bindings { + Bindings::Classic(bindings) => bindings.pragma_frag.create_expression(ctx), + Bindings::AutomaticScript(bindings) => { + let object_ident = bindings.require_jsx(ctx); + let property_name = Atom::from("Fragment"); + create_static_member_expression(object_ident, property_name, ctx) + } + Bindings::AutomaticModule(bindings) => { + let ident = bindings.import_fragment(ctx); + Expression::Identifier(ctx.alloc(ident)) + } + } + } + + fn get_create_element( + &mut self, + has_key_after_props_spread: bool, + jsxs: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match &mut self.bindings { + Bindings::Classic(bindings) => bindings.pragma.create_expression(ctx), + Bindings::AutomaticScript(bindings) => { + let (ident, property_name) = if has_key_after_props_spread { + (bindings.require_create_element(ctx), Atom::from("createElement")) + } else { + let property_name = if bindings.is_development { + Atom::from("jsxDEV") + } else if jsxs { + Atom::from("jsxs") + } else { + Atom::from("jsx") + }; + (bindings.require_jsx(ctx), property_name) + }; + create_static_member_expression(ident, property_name, ctx) + } + Bindings::AutomaticModule(bindings) => { + let ident = if has_key_after_props_spread { + bindings.import_create_element(ctx) + } else if jsxs { + bindings.import_jsxs(ctx) + } else { + bindings.import_jsx(ctx) + }; + Expression::Identifier(ctx.alloc(ident)) + } + } + } + + fn transform_jsx_member_expression( + expr: ArenaBox<'a, JSXMemberExpression<'a>>, + ctx: &TraverseCtx<'a>, + ) -> Expression<'a> { + let JSXMemberExpression { span, object, property } = expr.unbox(); + let object = match object { + JSXMemberExpressionObject::IdentifierReference(ident) => Expression::Identifier(ident), + JSXMemberExpressionObject::MemberExpression(expr) => { + Self::transform_jsx_member_expression(expr, ctx) + } + JSXMemberExpressionObject::ThisExpression(expr) => ctx.ast.expression_this(expr.span), + }; + let property = ctx.ast.identifier_name(property.span, property.name); + ctx.ast.member_expression_static(span, object, property, false).into() + } + + fn transform_jsx_attribute_value( + &mut self, + value: Option>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match value { + Some(JSXAttributeValue::StringLiteral(s)) => { + let mut decoded = None; + Self::decode_entities(s.value.as_str(), &mut decoded, s.value.len(), ctx); + let jsx_text = if let Some(decoded) = decoded { + // Text contains HTML entities which were decoded. + // `decoded` contains the decoded string as an `ArenaString`. Convert it to `Atom`. + Atom::from(decoded) + } else { + // No HTML entities needed to be decoded. Use the original `Atom` without copying. + s.value + }; + ctx.ast.expression_string_literal(s.span, jsx_text, None) + } + Some(JSXAttributeValue::Element(e)) => self.transform_jsx_element(e, ctx), + Some(JSXAttributeValue::Fragment(e)) => { + self.transform_jsx(e.span, None, e.unbox().children, ctx) + } + Some(JSXAttributeValue::ExpressionContainer(c)) => match c.unbox().expression { + jsx_expr @ match_expression!(JSXExpression) => jsx_expr.into_expression(), + JSXExpression::EmptyExpression(e) => { + ctx.ast.expression_boolean_literal(e.span, true) + } + }, + None => ctx.ast.expression_boolean_literal(SPAN, true), + } + } + + fn transform_jsx_child_automatic( + &mut self, + child: JSXChild<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // Align spread child behavior with esbuild. + // Instead of Babel throwing `Spread children are not supported in React.` + // `<>{...foo}` -> `jsxs(Fragment, { children: [ ...foo ] })` + if let JSXChild::Spread(e) = child { + let JSXSpreadChild { span, expression } = e.unbox(); + let spread_element = ctx.ast.array_expression_element_spread_element(span, expression); + let elements = ctx.ast.vec1(spread_element); + Some(ctx.ast.expression_array(span, elements)) + } else { + self.transform_jsx_child(child, ctx) + } + } + + fn transform_jsx_child_classic( + &mut self, + child: JSXChild<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // Align spread child behavior with esbuild. + // Instead of Babel throwing `Spread children are not supported in React.` + // `<>{...foo}` -> `React.createElement(React.Fragment, null, ...foo)` + if let JSXChild::Spread(e) = child { + let JSXSpreadChild { span, expression } = e.unbox(); + Some(ctx.ast.argument_spread_element(span, expression)) + } else { + self.transform_jsx_child(child, ctx).map(Argument::from) + } + } + + fn transform_jsx_child( + &mut self, + child: JSXChild<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + match child { + JSXChild::Text(text) => Self::transform_jsx_text(&text, ctx), + JSXChild::ExpressionContainer(e) => match e.unbox().expression { + jsx_expr @ match_expression!(JSXExpression) => Some(jsx_expr.into_expression()), + JSXExpression::EmptyExpression(_) => None, + }, + JSXChild::Element(e) => Some(self.transform_jsx_element(e, ctx)), + JSXChild::Fragment(e) => { + Some(self.transform_jsx(e.span, None, e.unbox().children, ctx)) + } + JSXChild::Spread(_) => unreachable!(), + } + } + + fn get_attribute_name(name: JSXAttributeName<'a>, ctx: &TraverseCtx<'a>) -> PropertyKey<'a> { + match name { + JSXAttributeName::Identifier(ident) => { + let name = ident.name; + if ident.name.contains('-') { + PropertyKey::from(ctx.ast.expression_string_literal(ident.span, name, None)) + } else { + ctx.ast.property_key_static_identifier(ident.span, name) + } + } + JSXAttributeName::NamespacedName(namespaced) => { + let name = ctx.ast.atom(&namespaced.to_string()); + PropertyKey::from(ctx.ast.expression_string_literal(namespaced.span, name, None)) + } + } + } + + fn transform_jsx_text(text: &JSXText<'a>, ctx: &TraverseCtx<'a>) -> Option> { + Self::fixup_whitespace_and_decode_entities(text.value, ctx) + .map(|value| ctx.ast.expression_string_literal(text.span, value, None)) + } + + /// JSX trims whitespace at the end and beginning of lines, except that the + /// start/end of a tag is considered a start/end of a line only if that line is + /// on the same line as the closing tag. See examples in + /// tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx + /// See also and + /// + /// An equivalent algorithm would be: + /// - If there is only one line, return it. + /// - If there is only whitespace (but multiple lines), return `undefined`. + /// - Split the text into lines. + /// - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines. + /// - Decode entities on each line (individually). + /// - Remove empty lines and join the rest with " ". + /// + /// + fn fixup_whitespace_and_decode_entities( + text: Atom<'a>, + ctx: &TraverseCtx<'a>, + ) -> Option> { + // Avoid copying strings in the common case where there's only 1 line of text, + // and it contains no HTML entities that need decoding. + // + // Where we do have to decode HTML entities, or concatenate multiple lines, assemble the + // concatenated text directly in arena, in an `ArenaString` (the accumulator `acc`), + // to avoid allocations. Initialize that `ArenaString` with capacity equal to length of + // the original text. This may be a bit more capacity than is required, once whitespace + // is removed, but it's highly unlikely to be insufficient capacity, so the `ArenaString` + // shouldn't need to reallocate while it's being constructed. + // + // When first line containing some text is found: + // * If it contains HTML entities, decode them and write decoded text to accumulator `acc`. + // * Otherwise, store trimmed text in `only_line` as an `Atom<'a>`. + // + // When another line containing some text is found: + // * If accumulator isn't already initialized, initialize it, starting with `only_line`. + // * Push a space to the accumulator. + // * Decode current line into the accumulator. + // + // At the end: + // * If accumulator is initialized, convert the `ArenaString` to an `Atom` and return it. + // * If `only_line` contains a string, that means only 1 line contained text, and that line + // didn't contain any HTML entities which needed decoding. + // So we can just return the `Atom` that's in `only_line` (without any copying). + + let mut acc: Option = None; + let mut only_line: Option> = None; + let mut first_non_whitespace: Option = Some(0); + let mut last_non_whitespace: Option = None; + for (index, c) in text.char_indices() { + if is_line_terminator(c) { + if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) { + Self::add_line_of_jsx_text( + Atom::from(&text.as_str()[first..last]), + &mut acc, + &mut only_line, + text.len(), + ctx, + ); + } + first_non_whitespace = None; + } else if !is_white_space_single_line(c) { + last_non_whitespace = Some(index + c.len_utf8()); + if first_non_whitespace.is_none() { + first_non_whitespace.replace(index); + } + } + } + + if let Some(first) = first_non_whitespace { + Self::add_line_of_jsx_text( + Atom::from(&text.as_str()[first..]), + &mut acc, + &mut only_line, + text.len(), + ctx, + ); + } + + if let Some(acc) = acc { Some(Atom::from(acc)) } else { only_line } + } + + fn add_line_of_jsx_text( + trimmed_line: Atom<'a>, + acc: &mut Option>, + only_line: &mut Option>, + text_len: usize, + ctx: &TraverseCtx<'a>, + ) { + if let Some(buffer) = acc.as_mut() { + // Already some text in accumulator. Push a space before this line is added to `acc`. + buffer.push(' '); + } else if let Some(only_line) = only_line.take() { + // This is the 2nd line containing text. Previous line did not contain any HTML entities. + // Generate an accumulator containing previous line and a trailing space. + // Current line will be added to the accumulator after it. + let mut buffer = ArenaStringBuilder::with_capacity_in(text_len, ctx.ast.allocator); + buffer.push_str(only_line.as_str()); + buffer.push(' '); + *acc = Some(buffer); + } + + // Decode any HTML entities in this line + Self::decode_entities(trimmed_line.as_str(), acc, text_len, ctx); + + if acc.is_none() { + // This is the first line containing text, and there are no HTML entities in this line. + // Record this line in `only_line`. + // If this turns out to be the only line, we won't need to construct an `ArenaString`, + // so avoid all copying. + *only_line = Some(trimmed_line); + } + } + + /// Replace entities like " ", "{", and "�" with the characters they encode. + /// * See + /// Code adapted from + /// + /// If either: + /// (a) Text contains any HTML entities that need to be decoded, or + /// (b) accumulator `acc` passed in to this method is `Some` + /// then push the decoded string to `acc` (initializing it first if required). + /// + /// Otherwise, leave `acc` as `None`. This indicates that the text contains no HTML entities. + /// Caller can use a slice of the original text, rather than making any copies. + fn decode_entities( + s: &str, + acc: &mut Option>, + text_len: usize, + ctx: &TraverseCtx<'a>, + ) { + let mut chars = s.char_indices(); + let mut prev = 0; + while let Some((i, c)) = chars.next() { + if c == '&' { + let mut start = i; + let mut end = None; + for (j, c) in chars.by_ref() { + if c == ';' { + end.replace(j); + break; + } else if c == '&' { + start = j; + } + } + if let Some(end) = end { + let buffer = acc.get_or_insert_with(|| { + ArenaStringBuilder::with_capacity_in(text_len, ctx.ast.allocator) + }); + + buffer.push_str(&s[prev..start]); + prev = end + 1; + let word = &s[start + 1..end]; + if let Some(decimal) = word.strip_prefix('#') { + if let Some(hex) = decimal.strip_prefix('x') { + if let Some(c) = + u32::from_str_radix(hex, 16).ok().and_then(char::from_u32) + { + // `ģ` + buffer.push(c); + continue; + } + } else if let Some(c) = decimal.parse::().ok().and_then(char::from_u32) + { + // `{` + buffer.push(c); + continue; + } + } else if let Some(c) = XML_ENTITIES.get(word) { + // e.g. `"e;`, `&` + buffer.push(*c); + continue; + } + // Fallback + buffer.push('&'); + buffer.push_str(word); + buffer.push(';'); + } else { + // Reached end of text without finding a `;` after the `&`. + // No point searching for a further `&`, so exit the loop. + break; + } + } + } + + if let Some(buffer) = acc.as_mut() { + buffer.push_str(&s[prev..]); + } + } + + /// The react jsx/jsxs transform falls back to `createElement` when an explicit `key` argument comes after a spread + /// + fn has_key_after_props_spread(opening_element: &JSXOpeningElement<'a>) -> bool { + let mut spread = false; + for attr in &opening_element.attributes { + if matches!(attr, JSXAttributeItem::SpreadAttribute(_)) { + spread = true; + } else if spread && matches!(attr, JSXAttributeItem::Attribute(a) if a.is_key()) { + return true; + } + } + false + } + + fn delete_reference_for_closing_element( + element: Option<&JSXClosingElement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(element) = &element + && let Some(ident) = element.name.get_identifier() + { + ctx.delete_reference_for_identifier(ident); + } + } +} + +/// Create `IdentifierReference` for var name in current scope which is read from +fn get_read_identifier_reference<'a>( + span: Span, + name: Atom<'a>, + ctx: &mut TraverseCtx<'a>, +) -> Expression<'a> { + let reference_id = ctx.create_reference_in_current_scope(name.as_str(), ReferenceFlags::Read); + let ident = ctx.ast.alloc_identifier_reference_with_reference_id(span, name, reference_id); + Expression::Identifier(ident) +} + +fn create_static_member_expression<'a>( + object_ident: IdentifierReference<'a>, + property_name: Atom<'a>, + ctx: &TraverseCtx<'a>, +) -> Expression<'a> { + let object = Expression::Identifier(ctx.alloc(object_ident)); + let property = ctx.ast.identifier_name(SPAN, property_name); + ctx.ast.member_expression_static(SPAN, object, property, false).into() +} + +/// Check if an `ObjectExpression` has a property called `__proto__`. +/// +/// Returns `true` for any of: +/// * `{ __proto__: ... }` +/// * `{ "__proto__": ... }` +/// * `{ ["__proto__"]: ... }` +/// +/// Also currently returns `true` for `{ [__proto__]: ... }`, but that's probably not correct. +/// TODO: Fix that. +fn has_proto(e: &ObjectExpression<'_>) -> bool { + e.properties.iter().any(|p| p.prop_name().is_some_and(|(name, _)| name == "__proto__")) +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use oxc_allocator::Allocator; + use oxc_ast::ast::Expression; + use oxc_semantic::Scoping; + use oxc_syntax::{node::NodeId, scope::ScopeFlags}; + use oxc_traverse::ReusableTraverseCtx; + + use super::Pragma; + use crate::{TransformCtx, TransformOptions, state::TransformState}; + + macro_rules! setup { + ($traverse_ctx:ident, $transform_ctx:ident) => { + let allocator = Allocator::default(); + + let mut scoping = Scoping::default(); + scoping.add_scope(None, NodeId::DUMMY, ScopeFlags::Top); + + let state = TransformState::default(); + let traverse_ctx = ReusableTraverseCtx::new(state, scoping, &allocator); + // SAFETY: Macro user only gets a `&mut TransCtx`, which cannot be abused + let mut traverse_ctx = unsafe { traverse_ctx.unwrap() }; + let $traverse_ctx = &mut traverse_ctx; + + let $transform_ctx = + TransformCtx::new(Path::new("test.jsx"), &TransformOptions::default()); + }; + } + + #[test] + fn default_pragma() { + setup!(traverse_ctx, transform_ctx); + + let pragma = None; + let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &transform_ctx); + let expr = pragma.create_expression(traverse_ctx); + + let Expression::StaticMemberExpression(member) = &expr else { panic!() }; + let Expression::Identifier(object) = &member.object else { panic!() }; + assert_eq!(object.name, "React"); + assert_eq!(member.property.name, "createElement"); + } + + #[test] + fn single_part_pragma() { + setup!(traverse_ctx, transform_ctx); + + let pragma = Some("single"); + let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &transform_ctx); + let expr = pragma.create_expression(traverse_ctx); + + let Expression::Identifier(ident) = &expr else { panic!() }; + assert_eq!(ident.name, "single"); + } + + #[test] + fn two_part_pragma() { + setup!(traverse_ctx, transform_ctx); + + let pragma = Some("first.second"); + let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &transform_ctx); + let expr = pragma.create_expression(traverse_ctx); + + let Expression::StaticMemberExpression(member) = &expr else { panic!() }; + let Expression::Identifier(object) = &member.object else { panic!() }; + assert_eq!(object.name, "first"); + assert_eq!(member.property.name, "second"); + } + + #[test] + fn multi_part_pragma() { + setup!(traverse_ctx, transform_ctx); + + let pragma = Some("first.second.third"); + let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &transform_ctx); + let expr = pragma.create_expression(traverse_ctx); + + let Expression::StaticMemberExpression(outer_member) = &expr else { panic!() }; + let Expression::StaticMemberExpression(inner_member) = &outer_member.object else { + panic!() + }; + let Expression::Identifier(object) = &inner_member.object else { panic!() }; + assert_eq!(object.name, "first"); + assert_eq!(inner_member.property.name, "second"); + assert_eq!(outer_member.property.name, "third"); + } + + #[test] + fn this_pragma() { + setup!(traverse_ctx, transform_ctx); + + let pragma = Some("this"); + let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &transform_ctx); + let expr = pragma.create_expression(traverse_ctx); + + assert!(matches!(&expr, Expression::ThisExpression(_))); + } + + #[test] + fn this_prop_pragma() { + setup!(traverse_ctx, transform_ctx); + + let pragma = Some("this.a.b"); + let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &transform_ctx); + let expr = pragma.create_expression(traverse_ctx); + + let Expression::StaticMemberExpression(outer_member) = &expr else { panic!() }; + let Expression::StaticMemberExpression(inner_member) = &outer_member.object else { + panic!() + }; + assert!(matches!(&inner_member.object, Expression::ThisExpression(_))); + assert_eq!(inner_member.property.name, "a"); + assert_eq!(outer_member.property.name, "b"); + } + + #[test] + fn import_meta_pragma() { + setup!(traverse_ctx, transform_ctx); + + let pragma = Some("import.meta"); + let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &transform_ctx); + let expr = pragma.create_expression(traverse_ctx); + + let Expression::MetaProperty(meta_prop) = &expr else { panic!() }; + assert_eq!(&meta_prop.meta.name, "import"); + assert_eq!(&meta_prop.property.name, "meta"); + } + + #[test] + fn import_meta_prop_pragma() { + setup!(traverse_ctx, transform_ctx); + + let pragma = Some("import.meta.prop"); + let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &transform_ctx); + let expr = pragma.create_expression(traverse_ctx); + + let Expression::StaticMemberExpression(member) = &expr else { panic!() }; + let Expression::MetaProperty(meta_prop) = &member.object else { panic!() }; + assert_eq!(&meta_prop.meta.name, "import"); + assert_eq!(&meta_prop.property.name, "meta"); + assert_eq!(member.property.name, "prop"); + } + + #[test] + fn entity_after_stray_amp() { + setup!(traverse_ctx, _transform_ctx); + let input = "& &"; + let mut acc = None; + super::JsxImpl::decode_entities(input, &mut acc, input.len(), traverse_ctx); + let out = acc.as_ref().unwrap().as_str(); + assert_eq!(out, "& &"); + } +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/jsx_self.rs b/crates/swc_ecma_transformer/oxc/jsx/jsx_self.rs new file mode 100644 index 000000000000..667de1b7623b --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/jsx_self.rs @@ -0,0 +1,124 @@ +//! React JSX Self +//! +//! This plugin adds `__self` attribute to JSX elements. +//! +//! > This plugin is included in `preset-react`. +//! +//! ## Example +//! +//! Input: +//! ```js +//!
foo
; +//! foo; +//! <>foo; +//! ``` +//! +//! Output: +//! ```js +//!
foo
; +//! foo; +//! <>foo; +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-react-jsx-self](https://babeljs.io/docs/babel-plugin-transform-react-jsx-self). +//! +//! ## References: +//! +//! * Babel plugin implementation: + +use oxc_ast::ast::*; +use oxc_diagnostics::OxcDiagnostic; +use oxc_span::{SPAN, Span}; +use oxc_traverse::{Ancestor, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +const SELF: &str = "__self"; + +pub struct JsxSelf<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> JsxSelf<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for JsxSelf<'a, '_> { + fn enter_jsx_opening_element( + &mut self, + elem: &mut JSXOpeningElement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.add_self_this_attribute(elem, ctx); + } +} + +impl<'a> JsxSelf<'a, '_> { + pub fn report_error(&self, span: Span) { + let error = OxcDiagnostic::warn("Duplicate __self prop found.").with_label(span); + self.ctx.error(error); + } + + fn is_inside_constructor(ctx: &TraverseCtx<'a>) -> bool { + for scope_id in ctx.ancestor_scopes() { + let flags = ctx.scoping().scope_flags(scope_id); + if flags.is_block() || flags.is_arrow() { + continue; + } + return flags.is_constructor(); + } + unreachable!(); // Always hit `Program` and exit before loop ends + } + + fn has_no_super_class(ctx: &TraverseCtx<'a>) -> bool { + for ancestor in ctx.ancestors() { + if let Ancestor::ClassBody(class) = ancestor { + return class.super_class().is_none(); + } + } + true + } + + pub fn get_object_property_kind_for_jsx_plugin( + ctx: &TraverseCtx<'a>, + ) -> ObjectPropertyKind<'a> { + let kind = PropertyKind::Init; + let key = ctx.ast.property_key_static_identifier(SPAN, SELF); + let value = ctx.ast.expression_this(SPAN); + ctx.ast.object_property_kind_object_property(SPAN, kind, key, value, false, false, false) + } + + pub fn can_add_self_attribute(ctx: &TraverseCtx<'a>) -> bool { + !Self::is_inside_constructor(ctx) || Self::has_no_super_class(ctx) + } + + /// `
` + /// ^^^^^^^^^^^^^ + fn add_self_this_attribute(&self, elem: &mut JSXOpeningElement<'a>, ctx: &TraverseCtx<'a>) { + // Check if `__self` attribute already exists + for item in &elem.attributes { + if let JSXAttributeItem::Attribute(attribute) = item + && let JSXAttributeName::Identifier(ident) = &attribute.name + && ident.name == SELF + { + self.report_error(ident.span); + return; + } + } + + let name = ctx.ast.jsx_attribute_name_identifier(SPAN, SELF); + let value = { + let jsx_expr = JSXExpression::from(ctx.ast.expression_this(SPAN)); + ctx.ast.jsx_attribute_value_expression_container(SPAN, jsx_expr) + }; + let attribute = ctx.ast.jsx_attribute_item_attribute(SPAN, name, Some(value)); + elem.attributes.push(attribute); + } +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/jsx_source.rs b/crates/swc_ecma_transformer/oxc/jsx/jsx_source.rs new file mode 100644 index 000000000000..f47a784931a1 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/jsx_source.rs @@ -0,0 +1,213 @@ +//! React JSX Source +//! +//! This plugin adds `__source` attribute to JSX elements. +//! +//! > This plugin is included in `preset-react`. +//! +//! ## Example +//! +//! Input: +//! ```js +//!
foo
; +//! foo; +//! <>foo; +//! ``` +//! +//! Output: +//! ```js +//! var _jsxFileName = "/test.js"; +//!
foo
; +//! foo; +//! <>foo; +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-react-jsx-source](https://babeljs.io/docs/babel-plugin-transform-react-jsx-source). +//! +//! ## References: +//! +//! * Babel plugin implementation: + +use oxc_ast::ast::*; +use oxc_data_structures::rope::{Rope, get_line_column}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_span::{SPAN, Span}; +use oxc_syntax::{number::NumberBase, symbol::SymbolFlags}; +use oxc_traverse::{BoundIdentifier, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +const SOURCE: &str = "__source"; +const FILE_NAME_VAR: &str = "jsxFileName"; + +pub struct JsxSource<'a, 'ctx> { + filename_var: Option>, + source_rope: Option, + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> JsxSource<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { filename_var: None, source_rope: None, ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for JsxSource<'a, '_> { + fn exit_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(stmt) = self.get_filename_var_statement(ctx) { + self.ctx.top_level_statements.insert_statement(stmt); + } + } + + fn enter_jsx_opening_element( + &mut self, + elem: &mut JSXOpeningElement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.add_source_attribute(elem, ctx); + } +} + +impl<'a> JsxSource<'a, '_> { + /// Get line and column from offset and source text. + /// + /// Line number starts at 1. + /// Column number is in UTF-16 characters, and starts at 1. + /// + /// This matches Babel's output. + pub fn get_line_column(&mut self, offset: u32) -> (u32, u32) { + let source_rope = + self.source_rope.get_or_insert_with(|| Rope::from_str(self.ctx.source_text)); + let (line, column) = get_line_column(source_rope, offset, self.ctx.source_text); + // line and column are zero-indexed, but we want 1-indexed + (line + 1, column + 1) + } + + pub fn get_object_property_kind_for_jsx_plugin( + &mut self, + line: u32, + column: u32, + ctx: &mut TraverseCtx<'a>, + ) -> ObjectPropertyKind<'a> { + let kind = PropertyKind::Init; + let key = ctx.ast.property_key_static_identifier(SPAN, SOURCE); + let value = self.get_source_object(line, column, ctx); + ctx.ast.object_property_kind_object_property(SPAN, kind, key, value, false, false, false) + } + + pub fn report_error(&self, span: Span) { + let error = OxcDiagnostic::warn("Duplicate __source prop found.").with_label(span); + self.ctx.error(error); + } + + /// `` + /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + fn add_source_attribute( + &mut self, + elem: &mut JSXOpeningElement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Don't add `__source` if this node was generated + if elem.span.is_unspanned() { + return; + } + + // Check if `__source` attribute already exists + for item in &elem.attributes { + if let JSXAttributeItem::Attribute(attribute) = item + && let JSXAttributeName::Identifier(ident) = &attribute.name + && ident.name == SOURCE + { + self.report_error(ident.span); + return; + } + } + + let key = ctx.ast.jsx_attribute_name_identifier(SPAN, SOURCE); + // TODO: We shouldn't calculate line + column from scratch each time as it's expensive. + // Build a table of byte indexes of each line's start on first usage, and save it. + // Then calculate line and column from that. + let (line, column) = self.get_line_column(elem.span.start); + let object = self.get_source_object(line, column, ctx); + let value = + ctx.ast.jsx_attribute_value_expression_container(SPAN, JSXExpression::from(object)); + let attribute_item = ctx.ast.jsx_attribute_item_attribute(SPAN, key, Some(value)); + elem.attributes.push(attribute_item); + } + + #[expect(clippy::cast_lossless)] + pub fn get_source_object( + &mut self, + line: u32, + column: u32, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let kind = PropertyKind::Init; + + let filename = { + let key = ctx.ast.property_key_static_identifier(SPAN, "fileName"); + let value = self.get_filename_var(ctx).create_read_expression(ctx); + ctx.ast + .object_property_kind_object_property(SPAN, kind, key, value, false, false, false) + }; + + let line_number = { + let key = ctx.ast.property_key_static_identifier(SPAN, "lineNumber"); + let value = + ctx.ast.expression_numeric_literal(SPAN, line as f64, None, NumberBase::Decimal); + ctx.ast + .object_property_kind_object_property(SPAN, kind, key, value, false, false, false) + }; + + let column_number = { + let key = ctx.ast.property_key_static_identifier(SPAN, "columnNumber"); + let value = + ctx.ast.expression_numeric_literal(SPAN, column as f64, None, NumberBase::Decimal); + ctx.ast + .object_property_kind_object_property(SPAN, kind, key, value, false, false, false) + }; + + let properties = ctx.ast.vec_from_array([filename, line_number, column_number]); + ctx.ast.expression_object(SPAN, properties) + } + + pub fn get_filename_var_statement(&self, ctx: &TraverseCtx<'a>) -> Option> { + let decl = self.get_filename_var_declarator(ctx)?; + + let var_decl = Statement::VariableDeclaration(ctx.ast.alloc_variable_declaration( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec1(decl), + false, + )); + Some(var_decl) + } + + pub fn get_filename_var_declarator( + &self, + ctx: &TraverseCtx<'a>, + ) -> Option> { + let filename_var = self.filename_var.as_ref()?; + + let id = filename_var.create_binding_pattern(ctx); + let source_path = ctx.ast.atom(&self.ctx.source_path.to_string_lossy()); + let init = ctx.ast.expression_string_literal(SPAN, source_path, None); + let decl = + ctx.ast.variable_declarator(SPAN, VariableDeclarationKind::Var, id, Some(init), false); + Some(decl) + } + + fn get_filename_var(&mut self, ctx: &mut TraverseCtx<'a>) -> &BoundIdentifier<'a> { + self.filename_var.get_or_insert_with(|| { + ctx.generate_uid_in_root_scope(FILE_NAME_VAR, SymbolFlags::FunctionScopedVariable) + }) + } +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/mod.rs b/crates/swc_ecma_transformer/oxc/jsx/mod.rs new file mode 100644 index 000000000000..1fe6d0e372e0 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/mod.rs @@ -0,0 +1,136 @@ +use oxc_ast::{AstBuilder, ast::*}; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + es2018::ObjectRestSpreadOptions, + state::TransformState, +}; + +mod comments; +mod diagnostics; +mod display_name; +mod jsx_impl; +mod jsx_self; +mod jsx_source; +mod options; +mod refresh; +pub use comments::update_options_with_comments; +use display_name::ReactDisplayName; +use jsx_impl::JsxImpl; +use jsx_self::JsxSelf; +pub use options::{JsxOptions, JsxRuntime, ReactRefreshOptions}; +use refresh::ReactRefresh; + +/// [Preset React](https://babel.dev/docs/babel-preset-react) +/// +/// This preset includes the following plugins: +/// +/// * [plugin-transform-react-jsx](https://babeljs.io/docs/babel-plugin-transform-react-jsx) +/// * [plugin-transform-react-jsx-self](https://babeljs.io/docs/babel-plugin-transform-react-jsx-self) +/// * [plugin-transform-react-jsx-source](https://babel.dev/docs/babel-plugin-transform-react-jsx-source) +/// * [plugin-transform-react-display-name](https://babeljs.io/docs/babel-plugin-transform-react-display-name) +pub struct Jsx<'a, 'ctx> { + implementation: JsxImpl<'a, 'ctx>, + display_name: ReactDisplayName<'a, 'ctx>, + refresh: ReactRefresh<'a, 'ctx>, + enable_jsx_plugin: bool, + display_name_plugin: bool, + self_plugin: bool, + source_plugin: bool, + refresh_plugin: bool, +} + +// Constructors +impl<'a, 'ctx> Jsx<'a, 'ctx> { + pub fn new( + mut options: JsxOptions, + object_rest_spread_options: Option, + ast: AstBuilder<'a>, + ctx: &'ctx TransformCtx<'a>, + ) -> Self { + if options.jsx_plugin || options.development { + options.conform(); + } + let JsxOptions { + jsx_plugin, display_name_plugin, jsx_self_plugin, jsx_source_plugin, .. + } = options; + let refresh = options.refresh.clone(); + Self { + implementation: JsxImpl::new(options, object_rest_spread_options, ast, ctx), + display_name: ReactDisplayName::new(ctx), + enable_jsx_plugin: jsx_plugin, + display_name_plugin, + self_plugin: jsx_self_plugin, + source_plugin: jsx_source_plugin, + refresh_plugin: refresh.is_some(), + refresh: ReactRefresh::new(&refresh.unwrap_or_default(), ast, ctx), + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for Jsx<'a, '_> { + fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.enable_jsx_plugin { + program.source_type = program.source_type.with_standard(true); + } + if self.refresh_plugin { + self.refresh.enter_program(program, ctx); + } + } + + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.refresh_plugin { + self.refresh.exit_program(program, ctx); + } + if self.enable_jsx_plugin { + self.implementation.exit_program(program, ctx); + } else if self.source_plugin { + self.implementation.jsx_source.exit_program(program, ctx); + } + } + + fn enter_call_expression( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.display_name_plugin { + self.display_name.enter_call_expression(call_expr, ctx); + } + + if self.refresh_plugin { + self.refresh.enter_call_expression(call_expr, ctx); + } + } + + fn enter_jsx_opening_element( + &mut self, + elem: &mut JSXOpeningElement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !self.enable_jsx_plugin { + if self.self_plugin && JsxSelf::can_add_self_attribute(ctx) { + self.implementation.jsx_self.enter_jsx_opening_element(elem, ctx); + } + if self.source_plugin { + self.implementation.jsx_source.enter_jsx_opening_element(elem, ctx); + } + } + } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.enable_jsx_plugin { + self.implementation.exit_expression(expr, ctx); + } + if self.refresh_plugin { + self.refresh.exit_expression(expr, ctx); + } + } + + fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if self.refresh_plugin { + self.refresh.exit_function(func, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/options.rs b/crates/swc_ecma_transformer/oxc/jsx/options.rs new file mode 100644 index 000000000000..dac670dfdee8 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/options.rs @@ -0,0 +1,210 @@ +use serde::Deserialize; + +#[inline] +fn default_as_true() -> bool { + true +} + +/// Decides which runtime to use. +/// +/// Auto imports the functions that JSX transpiles to. +/// classic does not automatic import anything. +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum JsxRuntime { + Classic, + /// The default runtime is switched to automatic in Babel 8. + #[default] + Automatic, +} + +impl JsxRuntime { + pub fn is_classic(self) -> bool { + self == Self::Classic + } + + pub fn is_automatic(self) -> bool { + self == Self::Automatic + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct JsxOptions { + #[serde(skip)] + pub jsx_plugin: bool, + + #[serde(skip)] + pub display_name_plugin: bool, + + #[serde(skip)] + pub jsx_self_plugin: bool, + + #[serde(skip)] + pub jsx_source_plugin: bool, + + // Both Runtimes + // + /// Decides which runtime to use. + pub runtime: JsxRuntime, + + /// This toggles behavior specific to development, such as adding __source and __self. + /// + /// Defaults to `false`. + pub development: bool, + + /// Toggles whether or not to throw an error if a XML namespaced tag name is used. + /// + /// Though the JSX spec allows this, it is disabled by default since React's JSX does not currently have support for it. + #[serde(default = "default_as_true")] + pub throw_if_namespace: bool, + + /// Enables `@babel/plugin-transform-react-pure-annotations`. + /// + /// It will mark top-level React method calls as pure for tree shaking. + /// + /// Defaults to `true`. + #[serde(default = "default_as_true")] + pub pure: bool, + + // React Automatic Runtime + // + /// Replaces the import source when importing functions. + /// + /// Defaults to `react`. + #[serde(default)] + pub import_source: Option, + + // React Classic Runtime + // + /// Replace the function used when compiling JSX expressions. + /// + /// It should be a qualified name (e.g. React.createElement) or an identifier (e.g. createElement). + /// + /// Note that the @jsx React.DOM pragma has been deprecated as of React v0.12 + /// + /// Defaults to `React.createElement`. + #[serde(default)] + pub pragma: Option, + + /// Replace the component used when compiling JSX fragments. It should be a valid JSX tag name. + /// + /// Defaults to `React.Fragment`. + #[serde(default)] + pub pragma_frag: Option, + + /// `useBuiltIns` is deprecated in Babel 8. + /// + /// This value is used to skip Babel tests, and is not used in oxc. + pub use_built_ins: Option, + + /// `useSpread` is deprecated in Babel 8. + /// + /// This value is used to skip Babel tests, and is not used in oxc. + pub use_spread: Option, + + /// Fast Refresh + pub refresh: Option, +} + +impl Default for JsxOptions { + fn default() -> Self { + Self::enable() + } +} + +impl JsxOptions { + pub fn conform(&mut self) { + if self.development { + self.jsx_plugin = true; + self.jsx_self_plugin = true; + self.jsx_source_plugin = true; + } + } + + pub fn enable() -> Self { + Self { + jsx_plugin: true, + display_name_plugin: true, + jsx_self_plugin: false, + jsx_source_plugin: false, + runtime: JsxRuntime::default(), + development: false, + throw_if_namespace: default_as_true(), + pure: default_as_true(), + import_source: None, + pragma: None, + pragma_frag: None, + use_built_ins: None, + use_spread: None, + refresh: None, + } + } + + pub fn disable() -> Self { + Self { + jsx_plugin: false, + display_name_plugin: false, + jsx_self_plugin: false, + jsx_source_plugin: false, + runtime: JsxRuntime::default(), + development: false, + throw_if_namespace: false, + pure: false, + import_source: None, + pragma: None, + pragma_frag: None, + use_built_ins: None, + use_spread: None, + refresh: None, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ReactRefreshOptions { + /// Specify the identifier of the refresh registration variable. + /// + /// Defaults to `$RefreshReg$`. + #[serde(default = "default_refresh_reg")] + pub refresh_reg: String, + + /// Specify the identifier of the refresh signature variable. + /// + /// Defaults to `$RefreshSig$`. + #[serde(default = "default_refresh_sig")] + pub refresh_sig: String, + + /// Controls whether to emit full signatures or use a more compact representation. + /// + /// When set to `true`, this option causes this plugin to emit full, readable signatures + /// for React components and hooks. This can be useful for debugging and development purposes. + /// + /// When set to `false` (default), the transformer will use a more compact representation. + /// Specifically, it generates a SHA-1 hash of the signature and then encodes it using Base64. + /// This process produces a deterministic, compact representation that's suitable for + /// production builds while still uniquely identifying components. + /// + /// Defaults to `false`. + #[serde(default)] + pub emit_full_signatures: bool, +} + +impl Default for ReactRefreshOptions { + fn default() -> Self { + Self { + refresh_reg: default_refresh_reg(), + refresh_sig: default_refresh_sig(), + emit_full_signatures: false, + } + } +} + +fn default_refresh_reg() -> String { + String::from("$RefreshReg$") +} + +fn default_refresh_sig() -> String { + String::from("$RefreshSig$") +} diff --git a/crates/swc_ecma_transformer/oxc/jsx/refresh.rs b/crates/swc_ecma_transformer/oxc/jsx/refresh.rs new file mode 100644 index 000000000000..19416ca23115 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/jsx/refresh.rs @@ -0,0 +1,994 @@ +use std::{collections::hash_map::Entry, iter, str}; + +use base64::{ + encoded_len as base64_encoded_len, + prelude::{BASE64_STANDARD, Engine}, +}; +use rustc_hash::{FxHashMap, FxHashSet}; +use sha1::{Digest, Sha1}; + +use oxc_allocator::{ + Address, CloneIn, GetAddress, StringBuilder as ArenaStringBuilder, TakeIn, Vec as ArenaVec, +}; +use oxc_ast::{AstBuilder, NONE, ast::*, match_expression}; +use oxc_ast_visit::{ + Visit, + walk::{walk_call_expression, walk_declaration}, +}; +use oxc_semantic::{ReferenceFlags, ScopeFlags, ScopeId, SymbolFlags, SymbolId}; +use oxc_span::{Atom, GetSpan, SPAN}; +use oxc_syntax::operator::AssignmentOperator; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +use super::options::ReactRefreshOptions; + +/// Parse a string into a `RefreshIdentifierResolver` and convert it into an `Expression` +#[derive(Debug)] +enum RefreshIdentifierResolver<'a> { + /// Simple IdentifierReference (e.g. `$RefreshReg$`) + Identifier(IdentifierReference<'a>), + /// StaticMemberExpression (object, property) (e.g. `window.$RefreshReg$`) + Member((IdentifierReference<'a>, IdentifierName<'a>)), + /// Used for `import.meta` expression (e.g. `import.meta.$RefreshReg$`) + Expression(Expression<'a>), +} + +impl<'a> RefreshIdentifierResolver<'a> { + /// Parses a string into a RefreshIdentifierResolver + pub fn parse(input: &str, ast: AstBuilder<'a>) -> Self { + let mut parts = input.split('.'); + + let first_part = parts.next().unwrap(); + let Some(second_part) = parts.next() else { + // Handle simple identifier reference + return Self::Identifier(ast.identifier_reference(SPAN, ast.atom(input))); + }; + + if first_part == "import" { + // Handle `import.meta.$RefreshReg$` expression + let mut expr = ast.expression_meta_property( + SPAN, + ast.identifier_name(SPAN, "import"), + ast.identifier_name(SPAN, ast.atom(second_part)), + ); + if let Some(property) = parts.next() { + expr = Expression::from(ast.member_expression_static( + SPAN, + expr, + ast.identifier_name(SPAN, ast.atom(property)), + false, + )); + } + return Self::Expression(expr); + } + + // Handle `window.$RefreshReg$` member expression + let object = ast.identifier_reference(SPAN, ast.atom(first_part)); + let property = ast.identifier_name(SPAN, ast.atom(second_part)); + Self::Member((object, property)) + } + + /// Converts the RefreshIdentifierResolver into an Expression + pub fn to_expression(&self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + match self { + Self::Identifier(ident) => { + let reference_id = ctx.create_unbound_reference(&ident.name, ReferenceFlags::Read); + ctx.ast.expression_identifier_with_reference_id( + ident.span, + ident.name, + reference_id, + ) + } + Self::Member((ident, property)) => { + let reference_id = ctx.create_unbound_reference(&ident.name, ReferenceFlags::Read); + let ident = ctx.ast.expression_identifier_with_reference_id( + ident.span, + ident.name, + reference_id, + ); + Expression::from(ctx.ast.member_expression_static( + SPAN, + ident, + property.clone(), + false, + )) + } + Self::Expression(expr) => expr.clone_in(ctx.ast.allocator), + } + } +} + +/// React Fast Refresh +/// +/// Transform React functional components to integrate Fast Refresh. +/// +/// References: +/// +/// * +/// * +pub struct ReactRefresh<'a, 'ctx> { + refresh_reg: RefreshIdentifierResolver<'a>, + refresh_sig: RefreshIdentifierResolver<'a>, + emit_full_signatures: bool, + ctx: &'ctx TransformCtx<'a>, + // States + registrations: Vec<(BoundIdentifier<'a>, Atom<'a>)>, + /// Used to wrap call expression with signature. + /// (eg: hoc(() => {}) -> _s1(hoc(_s1(() => {})))) + last_signature: Option<(BindingIdentifier<'a>, ArenaVec<'a, Argument<'a>>)>, + // (function_scope_id, key) + function_signature_keys: FxHashMap, + non_builtin_hooks_callee: FxHashMap>>>, + /// Used to determine which bindings are used in JSX calls. + used_in_jsx_bindings: FxHashSet, +} + +impl<'a, 'ctx> ReactRefresh<'a, 'ctx> { + pub fn new( + options: &ReactRefreshOptions, + ast: AstBuilder<'a>, + ctx: &'ctx TransformCtx<'a>, + ) -> Self { + Self { + refresh_reg: RefreshIdentifierResolver::parse(&options.refresh_reg, ast), + refresh_sig: RefreshIdentifierResolver::parse(&options.refresh_sig, ast), + emit_full_signatures: options.emit_full_signatures, + registrations: Vec::default(), + ctx, + last_signature: None, + function_signature_keys: FxHashMap::default(), + non_builtin_hooks_callee: FxHashMap::default(), + used_in_jsx_bindings: FxHashSet::default(), + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ReactRefresh<'a, '_> { + fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.used_in_jsx_bindings = UsedInJSXBindingsCollector::collect(program, ctx); + + let mut new_statements = ctx.ast.vec_with_capacity(program.body.len() * 2); + for mut statement in program.body.take_in(ctx.ast) { + let next_statement = self.process_statement(&mut statement, ctx); + new_statements.push(statement); + if let Some(assignment_expression) = next_statement { + new_statements.push(assignment_expression); + } + } + program.body = new_statements; + } + + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.registrations.is_empty() { + return; + } + + let var_decl = Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.vec(), // This is replaced at the end + false, + )); + + let mut variable_declarator_items = ctx.ast.vec_with_capacity(self.registrations.len()); + let calls = self.registrations.iter().map(|(binding, persistent_id)| { + variable_declarator_items.push(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + binding.create_binding_pattern(ctx), + None, + false, + )); + + let callee = self.refresh_reg.to_expression(ctx); + let arguments = ctx.ast.vec_from_array([ + Argument::from(binding.create_read_expression(ctx)), + Argument::from(ctx.ast.expression_string_literal(SPAN, *persistent_id, None)), + ]); + ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_call(SPAN, callee, NONE, arguments, false), + ) + }); + + let var_decl_index = program.body.len(); + program.body.extend(iter::once(var_decl).chain(calls)); + + let Statement::VariableDeclaration(var_decl) = &mut program.body[var_decl_index] else { + unreachable!() + }; + var_decl.declarations = variable_declarator_items; + } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let signature = match expr { + Expression::FunctionExpression(func) => self.create_signature_call_expression( + func.scope_id(), + func.body.as_mut().unwrap(), + ctx, + ), + Expression::ArrowFunctionExpression(arrow) => { + let call_fn = + self.create_signature_call_expression(arrow.scope_id(), &mut arrow.body, ctx); + + // If the signature is found, we will push a new statement to the arrow function body. So it's not an expression anymore. + if call_fn.is_some() { + Self::transform_arrow_function_to_block(arrow, ctx); + } + call_fn + } + // hoc(_c = function() { }) + Expression::AssignmentExpression(_) => return, + // hoc1(hoc2(...)) + Expression::CallExpression(_) => self.last_signature.take(), + _ => None, + }; + + let Some((binding_identifier, mut arguments)) = signature else { + return; + }; + let binding = BoundIdentifier::from_binding_ident(&binding_identifier); + + if !matches!(expr, Expression::CallExpression(_)) { + // Try to get binding from parent VariableDeclarator + if let Ancestor::VariableDeclaratorInit(declarator) = ctx.parent() + && let Some(ident) = declarator.id().get_binding_identifier() + { + let id_binding = BoundIdentifier::from_binding_ident(ident); + self.handle_function_in_variable_declarator(&id_binding, &binding, arguments, ctx); + return; + } + } + + let mut found_call_expression = false; + for ancestor in ctx.ancestors() { + if ancestor.is_assignment_expression() { + continue; + } + if ancestor.is_call_expression() { + found_call_expression = true; + } + break; + } + + if found_call_expression { + self.last_signature = + Some((binding_identifier.clone(), arguments.clone_in(ctx.ast.allocator))); + } + + arguments.insert(0, Argument::from(expr.take_in(ctx.ast))); + *expr = ctx.ast.expression_call( + SPAN, + binding.create_read_expression(ctx), + NONE, + arguments, + false, + ); + } + + fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if !func.is_function_declaration() { + return; + } + + let Some((binding_identifier, mut arguments)) = self.create_signature_call_expression( + func.scope_id(), + func.body.as_mut().unwrap(), + ctx, + ) else { + return; + }; + + let Some(id) = func.id.as_ref() else { + return; + }; + let id_binding = BoundIdentifier::from_binding_ident(id); + + arguments.insert(0, Argument::from(id_binding.create_read_expression(ctx))); + + let binding = BoundIdentifier::from_binding_ident(&binding_identifier); + let callee = binding.create_read_expression(ctx); + let expr = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false); + let statement = ctx.ast.statement_expression(SPAN, expr); + + // Get the address of the statement containing this `FunctionDeclaration` + let address = match ctx.parent() { + // For `export function Foo() {}` + // which is a `Statement::ExportNamedDeclaration` + Ancestor::ExportNamedDeclarationDeclaration(decl) => decl.address(), + // For `export default function() {}` + // which is a `Statement::ExportDefaultDeclaration` + Ancestor::ExportDefaultDeclarationDeclaration(decl) => decl.address(), + // Otherwise just a `function Foo() {}` + // which is a `Statement::FunctionDeclaration`. + // `Function` is always stored in a `Box`, so has a stable memory address. + _ => Address::from_ref(func), + }; + self.ctx.statement_injector.insert_after(&address, statement); + } + + fn enter_call_expression( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let current_scope_id = ctx.current_scope_id(); + if !ctx.scoping().scope_flags(current_scope_id).is_function() { + return; + } + + let hook_name = match &call_expr.callee { + Expression::Identifier(ident) => ident.name, + Expression::StaticMemberExpression(member) => member.property.name, + _ => return, + }; + + if !is_use_hook_name(&hook_name) { + return; + } + + if !is_builtin_hook(&hook_name) { + // Check if a corresponding binding exists where we emit the signature. + let (binding_name, is_member_expression) = match &call_expr.callee { + Expression::Identifier(ident) => (Some(ident.name), false), + Expression::StaticMemberExpression(member) => { + if let Expression::Identifier(object) = &member.object { + (Some(object.name), true) + } else { + (None, false) + } + } + _ => unreachable!(), + }; + + if let Some(binding_name) = binding_name { + self.non_builtin_hooks_callee.entry(current_scope_id).or_default().push( + ctx.scoping() + .find_binding( + ctx.scoping().scope_parent_id(ctx.current_scope_id()).unwrap(), + binding_name.as_str(), + ) + .map(|symbol_id| { + let mut expr = ctx.create_bound_ident_expr( + SPAN, + binding_name, + symbol_id, + ReferenceFlags::Read, + ); + + if is_member_expression { + // binding_name.hook_name + expr = Expression::from(ctx.ast.member_expression_static( + SPAN, + expr, + ctx.ast.identifier_name(SPAN, hook_name), + false, + )); + } + expr + }), + ); + } + } + + let declarator_id = if let Ancestor::VariableDeclaratorInit(declarator) = ctx.parent() { + // TODO: if there is no LHS, consider some other heuristic. + declarator.id().span().source_text(self.ctx.source_text) + } else { + "" + }; + + let args = &call_expr.arguments; + let (args_key, mut key_len) = if hook_name == "useState" && !args.is_empty() { + let args_key = args[0].span().source_text(self.ctx.source_text); + (args_key, args_key.len() + 4) + } else if hook_name == "useReducer" && args.len() > 1 { + let args_key = args[1].span().source_text(self.ctx.source_text); + (args_key, args_key.len() + 4) + } else { + ("", 2) + }; + + key_len += hook_name.len() + declarator_id.len(); + + let string = match self.function_signature_keys.entry(current_scope_id) { + Entry::Occupied(entry) => { + let string = entry.into_mut(); + string.reserve(key_len + 2); + string.push_str("\\n"); + string + } + Entry::Vacant(entry) => entry.insert(String::with_capacity(key_len)), + }; + + // `hook_name{{declarator_id(args_key)}}` or `hook_name{{declarator_id}}` + let old_len = string.len(); + + string.push_str(&hook_name); + string.push('{'); + string.push_str(declarator_id); + if !args_key.is_empty() { + string.push('('); + string.push_str(args_key); + string.push(')'); + } + string.push('}'); + + debug_assert_eq!(key_len, string.len() - old_len); + } +} + +// Internal Methods +impl<'a> ReactRefresh<'a, '_> { + fn create_registration( + &mut self, + persistent_id: Atom<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> AssignmentTarget<'a> { + let binding = ctx.generate_uid_in_root_scope("c", SymbolFlags::FunctionScopedVariable); + let target = binding.create_target(ReferenceFlags::Write, ctx); + self.registrations.push((binding, persistent_id)); + target + } + + /// Similar to the `findInnerComponents` function in `react-refresh/babel`. + fn replace_inner_components( + &mut self, + inferred_name: &str, + expr: &mut Expression<'a>, + is_variable_declarator: bool, + ctx: &mut TraverseCtx<'a>, + ) -> bool { + match expr { + Expression::Identifier(ident) => { + // For case like: + // export const Something = hoc(Foo) + // we don't want to wrap Foo inside the call. + // Instead we assume it's registered at definition. + return is_componentish_name(&ident.name); + } + Expression::FunctionExpression(_) => {} + Expression::ArrowFunctionExpression(arrow) => { + // Don't transform `() => () => {}` + if arrow + .get_expression() + .is_some_and(|expr| matches!(expr, Expression::ArrowFunctionExpression(_))) + { + return false; + } + } + Expression::CallExpression(call_expr) => { + let allowed_callee = matches!( + call_expr.callee, + Expression::Identifier(_) + | Expression::ComputedMemberExpression(_) + | Expression::StaticMemberExpression(_) + ); + + if allowed_callee { + let callee_span = call_expr.callee.span(); + + let Some(argument_expr) = + call_expr.arguments.first_mut().and_then(|e| e.as_expression_mut()) + else { + return false; + }; + + let found_inside = self.replace_inner_components( + format!( + "{}${}", + inferred_name, + callee_span.source_text(self.ctx.source_text) + ) + .as_str(), + argument_expr, + /* is_variable_declarator */ false, + ctx, + ); + + if !found_inside { + return false; + } + + // const Foo = hoc1(hoc2(() => {})) + // export default memo(React.forwardRef(function() {})) + if is_variable_declarator { + return true; + } + } else { + return false; + } + } + _ => { + return false; + } + } + + if !is_variable_declarator { + *expr = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + self.create_registration(ctx.ast.atom(inferred_name), ctx), + expr.take_in(ctx.ast), + ); + } + + true + } + + /// _c = id.name; + fn create_assignment_expression( + &mut self, + id: &BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let left = self.create_registration(id.name, ctx); + let right = + ctx.create_bound_ident_expr(SPAN, id.name, id.symbol_id(), ReferenceFlags::Read); + let expr = ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, left, right); + ctx.ast.statement_expression(SPAN, expr) + } + + fn create_signature_call_expression( + &mut self, + scope_id: ScopeId, + body: &mut FunctionBody<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option<(BindingIdentifier<'a>, ArenaVec<'a, Argument<'a>>)> { + let key = self.function_signature_keys.remove(&scope_id)?; + + let key = if self.emit_full_signatures { + ctx.ast.atom(&key) + } else { + // Prefer to hash when we can (e.g. outside of ASTExplorer). + // This makes it deterministically compact, even if there's + // e.g. a useState initializer with some code inside. + // We also need it for www that has transforms like cx() + // that don't understand if something is part of a string. + const SHA1_HASH_LEN: usize = 20; + const ENCODED_LEN: usize = { + let len = base64_encoded_len(SHA1_HASH_LEN, true); + match len { + Some(l) => l, + None => panic!("Invalid base64 length"), + } + }; + + let mut hasher = Sha1::new(); + hasher.update(&key); + let hash = hasher.finalize(); + debug_assert_eq!(hash.len(), SHA1_HASH_LEN); + + // Encode to base64 string directly in arena, without an intermediate string allocation + #[expect(clippy::items_after_statements)] + const ZEROS_STR: &str = { + const ZEROS_BYTES: [u8; ENCODED_LEN] = [0; ENCODED_LEN]; + match str::from_utf8(&ZEROS_BYTES) { + Ok(s) => s, + Err(_) => unreachable!(), + } + }; + + let mut hashed_key = ArenaStringBuilder::from_str_in(ZEROS_STR, ctx.ast.allocator); + // SAFETY: Base64 encoding only produces ASCII bytes. Even if our assumptions are incorrect, + // and Base64 bytes do not fill `hashed_key` completely, the remaining bytes are 0, so also ASCII. + let hashed_key_bytes = unsafe { hashed_key.as_mut_str().as_bytes_mut() }; + let encoded_bytes = BASE64_STANDARD.encode_slice(hash, hashed_key_bytes).unwrap(); + debug_assert_eq!(encoded_bytes, ENCODED_LEN); + Atom::from(hashed_key) + }; + + let callee_list = self.non_builtin_hooks_callee.remove(&scope_id).unwrap_or_default(); + let callee_len = callee_list.len(); + let custom_hooks_in_scope = ctx.ast.vec_from_iter( + callee_list.into_iter().filter_map(|e| e.map(ArrayExpressionElement::from)), + ); + + let force_reset = custom_hooks_in_scope.len() != callee_len; + + let mut arguments = ctx.ast.vec(); + arguments.push(Argument::from(ctx.ast.expression_string_literal(SPAN, key, None))); + + if force_reset || !custom_hooks_in_scope.is_empty() { + arguments.push(Argument::from(ctx.ast.expression_boolean_literal(SPAN, force_reset))); + } + + if !custom_hooks_in_scope.is_empty() { + // function () { return custom_hooks_in_scope } + let formal_parameters = ctx.ast.formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + ctx.ast.vec(), + NONE, + ); + let function_body = ctx.ast.function_body( + SPAN, + ctx.ast.vec(), + ctx.ast.vec1(ctx.ast.statement_return( + SPAN, + Some(ctx.ast.expression_array(SPAN, custom_hooks_in_scope)), + )), + ); + let scope_id = ctx.create_child_scope_of_current(ScopeFlags::Function); + let function = + Argument::from(ctx.ast.expression_function_with_scope_id_and_pure_and_pife( + SPAN, + FunctionType::FunctionExpression, + None, + false, + false, + false, + NONE, + NONE, + formal_parameters, + NONE, + Some(function_body), + scope_id, + false, + false, + )); + arguments.push(function); + } + + // _s = refresh_sig(); + let init = ctx.ast.expression_call( + SPAN, + self.refresh_sig.to_expression(ctx), + NONE, + ctx.ast.vec(), + false, + ); + let binding = self.ctx.var_declarations.create_uid_var_with_init("s", init, ctx); + + // _s(); + let call_expression = ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_call( + SPAN, + binding.create_read_expression(ctx), + NONE, + ctx.ast.vec(), + false, + ), + ); + + body.statements.insert(0, call_expression); + + // Following is the signature call expression, will be generated in call site. + // _s(App, signature_key, false, function() { return [] }); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ custom hooks only + let binding_identifier = binding.create_binding_identifier(ctx); + Some((binding_identifier, arguments)) + } + + fn process_statement( + &mut self, + statement: &mut Statement<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + match statement { + Statement::VariableDeclaration(variable) => { + self.handle_variable_declaration(variable, ctx) + } + Statement::FunctionDeclaration(func) => self.handle_function_declaration(func, ctx), + Statement::ExportNamedDeclaration(export_decl) => { + if let Some(declaration) = &mut export_decl.declaration { + match declaration { + Declaration::FunctionDeclaration(func) => { + self.handle_function_declaration(func, ctx) + } + Declaration::VariableDeclaration(variable) => { + self.handle_variable_declaration(variable, ctx) + } + _ => None, + } + } else { + None + } + } + Statement::ExportDefaultDeclaration(stmt_decl) => { + match &mut stmt_decl.declaration { + declaration @ match_expression!(ExportDefaultDeclarationKind) => { + let expression = declaration.to_expression_mut(); + if !matches!(expression, Expression::CallExpression(_)) { + // For now, we only support possible HOC calls here. + // Named function declarations are handled in FunctionDeclaration. + // Anonymous direct exports like export default function() {} + // are currently ignored. + return None; + } + + // This code path handles nested cases like: + // export default memo(() => {}) + // In those cases it is more plausible people will omit names + // so they're worth handling despite possible false positives. + // More importantly, it handles the named case: + // export default memo(function Named() {}) + self.replace_inner_components( + "%default%", + expression, + /* is_variable_declarator */ false, + ctx, + ); + + None + } + ExportDefaultDeclarationKind::FunctionDeclaration(func) => { + if let Some(id) = &func.id { + if func.is_typescript_syntax() || !is_componentish_name(&id.name) { + return None; + } + + return Some(self.create_assignment_expression(id, ctx)); + } + None + } + _ => None, + } + } + _ => None, + } + } + + fn handle_function_declaration( + &mut self, + func: &Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let Some(id) = &func.id else { + return None; + }; + + if func.is_typescript_syntax() || !is_componentish_name(&id.name) { + return None; + } + + Some(self.create_assignment_expression(id, ctx)) + } + + fn handle_variable_declaration( + &mut self, + decl: &mut VariableDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if decl.declarations.len() != 1 { + return None; + } + + let declarator = decl.declarations.first_mut().unwrap_or_else(|| unreachable!()); + let init = declarator.init.as_mut()?; + let id = declarator.id.get_binding_identifier()?; + let symbol_id = id.symbol_id(); + + if !is_componentish_name(&id.name) { + return None; + } + + match init { + // Likely component definitions. + Expression::ArrowFunctionExpression(arrow) => { + // () => () => {} + if arrow.get_expression().is_some_and(|expr| matches!(expr, Expression::ArrowFunctionExpression(_))) { + return None; + } + } + Expression::FunctionExpression(_) + // Maybe something like styled.div`...` + | Expression::TaggedTemplateExpression(_) => { + // Special case when a variable would get an inferred name: + // let Foo = () => {} + // let Foo = function() {} + // let Foo = styled.div``; + // We'll register it on next line so that + // we don't mess up the inferred 'Foo' function name. + // (eg: with @babel/plugin-transform-react-display-name or + // babel-plugin-styled-components) + } + Expression::CallExpression(call_expr) => { + let is_import_expression = match call_expr.callee.get_inner_expression() { + Expression::ImportExpression(_) => { + true + } + Expression::Identifier(ident) => { + ident.name.starts_with("require") + }, + _ => false + }; + + if is_import_expression { + return None; + } + } + _ => { + return None; + } + } + + // Maybe a HOC. + // Try to determine if this is some form of import. + let found_inside = self + .replace_inner_components(&id.name, init, /* is_variable_declarator */ true, ctx); + + if !found_inside && !self.used_in_jsx_bindings.contains(&symbol_id) { + return None; + } + + Some(self.create_assignment_expression(id, ctx)) + } + + /// Handle `export const Foo = () => {}` or `const Foo = function() {}` + fn handle_function_in_variable_declarator( + &self, + id_binding: &BoundIdentifier<'a>, + binding: &BoundIdentifier<'a>, + mut arguments: ArenaVec<'a, Argument<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + // Special case when a function would get an inferred name: + // let Foo = () => {} + // let Foo = function() {} + // We'll add signature it on next line so that + // we don't mess up the inferred 'Foo' function name. + + // Result: let Foo = () => {}; __signature(Foo, ...); + arguments.insert(0, Argument::from(id_binding.create_read_expression(ctx))); + let statement = ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_call( + SPAN, + binding.create_read_expression(ctx), + NONE, + arguments, + false, + ), + ); + + // Get the address of the statement containing this `VariableDeclarator` + let address = + if let Ancestor::ExportNamedDeclarationDeclaration(export_decl) = ctx.ancestor(2) { + // For `export const Foo = () => {}` + // which is a `VariableDeclaration` inside a `Statement::ExportNamedDeclaration` + export_decl.address() + } else { + // Otherwise just a `const Foo = () => {}` which is a `Statement::VariableDeclaration` + let var_decl = ctx.ancestor(1); + debug_assert!(matches!(var_decl, Ancestor::VariableDeclarationDeclarations(_))); + var_decl.address() + }; + self.ctx.statement_injector.insert_after(&address, statement); + } + + /// Convert arrow function expression to normal arrow function + /// + /// ```js + /// () => 1 + /// ``` + /// to + /// ```js + /// () => { return 1 } + /// ``` + fn transform_arrow_function_to_block( + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &TraverseCtx<'a>, + ) { + if !arrow.expression { + return; + } + + arrow.expression = false; + + let Some(Statement::ExpressionStatement(statement)) = arrow.body.statements.pop() else { + unreachable!("arrow function body is never empty") + }; + + arrow + .body + .statements + .push(ctx.ast.statement_return(SPAN, Some(statement.unbox().expression))); + } +} + +fn is_componentish_name(name: &str) -> bool { + name.as_bytes().first().is_some_and(u8::is_ascii_uppercase) +} + +fn is_use_hook_name(name: &str) -> bool { + name.starts_with("use") && name.as_bytes().get(3).is_none_or(u8::is_ascii_uppercase) +} + +#[rustfmt::skip] +fn is_builtin_hook(hook_name: &str) -> bool { + matches!( + hook_name, + "useState" | "useReducer" | "useEffect" | + "useLayoutEffect" | "useMemo" | "useCallback" | + "useRef" | "useContext" | "useImperativeHandle" | + "useDebugValue" | "useId" | "useDeferredValue" | + "useTransition" | "useInsertionEffect" | "useSyncExternalStore" | + "useFormStatus" | "useFormState" | "useActionState" | + "useOptimistic" + ) +} + +/// Collects all bindings that are used in JSX elements or JSX-like calls. +/// +/// For +struct UsedInJSXBindingsCollector<'a, 'b> { + ctx: &'b TraverseCtx<'a>, + bindings: FxHashSet, +} + +impl<'a, 'b> UsedInJSXBindingsCollector<'a, 'b> { + fn collect(program: &Program<'a>, ctx: &'b TraverseCtx<'a>) -> FxHashSet { + let mut visitor = Self { ctx, bindings: FxHashSet::default() }; + visitor.visit_program(program); + visitor.bindings + } + + fn is_jsx_like_call(name: &str) -> bool { + matches!(name, "createElement" | "jsx" | "jsxDEV" | "jsxs") + } +} + +impl<'a> Visit<'a> for UsedInJSXBindingsCollector<'a, '_> { + fn visit_call_expression(&mut self, it: &CallExpression<'a>) { + walk_call_expression(self, it); + + let is_jsx_call = match &it.callee { + Expression::Identifier(ident) => Self::is_jsx_like_call(&ident.name), + Expression::StaticMemberExpression(member) => { + Self::is_jsx_like_call(&member.property.name) + } + _ => false, + }; + + if is_jsx_call + && let Some(Argument::Identifier(ident)) = it.arguments.first() + && let Some(symbol_id) = + self.ctx.scoping().get_reference(ident.reference_id()).symbol_id() + { + self.bindings.insert(symbol_id); + } + } + + fn visit_jsx_opening_element(&mut self, it: &JSXOpeningElement<'_>) { + if let Some(ident) = it.name.get_identifier() + && let Some(symbol_id) = + self.ctx.scoping().get_reference(ident.reference_id()).symbol_id() + { + self.bindings.insert(symbol_id); + } + } + + #[inline] + fn visit_ts_type_annotation(&mut self, _it: &TSTypeAnnotation<'a>) { + // Skip type annotations because it definitely doesn't have any JSX bindings + } + + #[inline] + fn visit_declaration(&mut self, it: &Declaration<'a>) { + if matches!( + it, + Declaration::TSTypeAliasDeclaration(_) | Declaration::TSInterfaceDeclaration(_) + ) { + // Skip type-only declarations because it definitely doesn't have any JSX bindings + return; + } + walk_declaration(self, it); + } + + #[inline] + fn visit_import_declaration(&mut self, _it: &ImportDeclaration<'a>) { + // Skip import declarations because it definitely doesn't have any JSX bindings + } + + #[inline] + fn visit_export_all_declaration(&mut self, _it: &ExportAllDeclaration<'a>) { + // Skip export all declarations because it definitely doesn't have any JSX bindings + } +} diff --git a/crates/swc_ecma_transformer/oxc/lib.rs b/crates/swc_ecma_transformer/oxc/lib.rs new file mode 100644 index 000000000000..9b2c855b1020 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/lib.rs @@ -0,0 +1,734 @@ +//! Transformer / Transpiler +//! +//! References: +//! * +//! * +//! * + +use std::path::Path; + +use oxc_allocator::{Allocator, TakeIn, Vec as ArenaVec}; +use oxc_ast::{AstBuilder, ast::*}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_semantic::Scoping; +use oxc_span::SPAN; +use oxc_traverse::{Traverse, traverse_mut}; + +// Core +mod common; +mod compiler_assumptions; +mod context; +mod options; +mod state; +mod utils; + +// Presets: +mod es2015; +mod es2016; +mod es2017; +mod es2018; +mod es2019; +mod es2020; +mod es2021; +mod es2022; +mod es2026; +mod jsx; +mod proposals; +mod regexp; +mod typescript; + +mod decorator; +mod plugins; + +use common::Common; +use context::{TransformCtx, TraverseCtx}; +use decorator::Decorator; +use es2015::ES2015; +use es2016::ES2016; +use es2017::ES2017; +use es2018::ES2018; +use es2019::ES2019; +use es2020::ES2020; +use es2021::ES2021; +use es2022::ES2022; +use es2026::ES2026; +use jsx::Jsx; +use regexp::RegExp; +use rustc_hash::FxHashMap; +use state::TransformState; +use typescript::TypeScript; + +use crate::plugins::Plugins; +pub use crate::{ + common::helper_loader::{Helper, HelperLoaderMode, HelperLoaderOptions}, + compiler_assumptions::CompilerAssumptions, + decorator::DecoratorOptions, + es2015::{ArrowFunctionsOptions, ES2015Options}, + es2016::ES2016Options, + es2017::ES2017Options, + es2018::ES2018Options, + es2019::ES2019Options, + es2020::ES2020Options, + es2021::ES2021Options, + es2022::{ClassPropertiesOptions, ES2022Options}, + es2026::ES2026Options, + jsx::{JsxOptions, JsxRuntime, ReactRefreshOptions}, + options::{ + ESTarget, Engine, EngineTargets, EnvOptions, Module, TransformOptions, + babel::{BabelEnvOptions, BabelOptions}, + }, + plugins::{PluginsOptions, StyledComponentsOptions}, + proposals::ProposalOptions, + typescript::{RewriteExtensionsMode, TypeScriptOptions}, +}; + +#[non_exhaustive] +pub struct TransformerReturn { + pub errors: std::vec::Vec, + pub scoping: Scoping, + /// Helpers used by this transform. + #[deprecated = "Internal usage only"] + pub helpers_used: FxHashMap, +} + +pub struct Transformer<'a> { + ctx: TransformCtx<'a>, + allocator: &'a Allocator, + + typescript: TypeScriptOptions, + decorator: DecoratorOptions, + plugins: PluginsOptions, + jsx: JsxOptions, + env: EnvOptions, + #[expect(dead_code)] + proposals: ProposalOptions, +} + +impl<'a> Transformer<'a> { + pub fn new(allocator: &'a Allocator, source_path: &Path, options: &TransformOptions) -> Self { + let ctx = TransformCtx::new(source_path, options); + Self { + ctx, + allocator, + typescript: options.typescript.clone(), + decorator: options.decorator, + plugins: options.plugins.clone(), + jsx: options.jsx.clone(), + env: options.env, + proposals: options.proposals, + } + } + + pub fn build_with_scoping( + mut self, + scoping: Scoping, + program: &mut Program<'a>, + ) -> TransformerReturn { + let allocator = self.allocator; + let ast_builder = AstBuilder::new(allocator); + + self.ctx.source_type = program.source_type; + self.ctx.source_text = program.source_text; + + if program.source_type.is_jsx() { + jsx::update_options_with_comments( + &program.comments, + &mut self.typescript, + &mut self.jsx, + &self.ctx, + ); + } + + let mut transformer = TransformerImpl { + common: Common::new(&self.env, &self.ctx), + decorator: Decorator::new(self.decorator, &self.ctx), + plugins: Plugins::new(self.plugins, &self.ctx), + x0_typescript: program + .source_type + .is_typescript() + .then(|| TypeScript::new(&self.typescript, &self.ctx)), + x1_jsx: Jsx::new(self.jsx, self.env.es2018.object_rest_spread, ast_builder, &self.ctx), + x2_es2026: ES2026::new(self.env.es2026, &self.ctx), + x2_es2022: ES2022::new( + self.env.es2022, + !self.typescript.allow_declare_fields + || self.typescript.remove_class_fields_without_initializer, + &self.ctx, + ), + x2_es2021: ES2021::new(self.env.es2021, &self.ctx), + x2_es2020: ES2020::new(self.env.es2020, &self.ctx), + x2_es2019: ES2019::new(self.env.es2019), + x2_es2018: ES2018::new(self.env.es2018, &self.ctx), + x2_es2016: ES2016::new(self.env.es2016, &self.ctx), + x2_es2017: ES2017::new(self.env.es2017, &self.ctx), + x3_es2015: ES2015::new(self.env.es2015, &self.ctx), + x4_regexp: RegExp::new(self.env.regexp, &self.ctx), + }; + + let state = TransformState::default(); + let scoping = traverse_mut(&mut transformer, allocator, program, scoping, state); + let helpers_used = self.ctx.helper_loader.used_helpers.borrow_mut().drain().collect(); + #[expect(deprecated)] + TransformerReturn { errors: self.ctx.take_errors(), scoping, helpers_used } + } +} + +struct TransformerImpl<'a, 'ctx> { + // NOTE: all callbacks must run in order. + x0_typescript: Option>, + decorator: Decorator<'a, 'ctx>, + plugins: Plugins<'a, 'ctx>, + x1_jsx: Jsx<'a, 'ctx>, + x2_es2026: ES2026<'a, 'ctx>, + x2_es2022: ES2022<'a, 'ctx>, + x2_es2021: ES2021<'a, 'ctx>, + x2_es2020: ES2020<'a, 'ctx>, + x2_es2019: ES2019, + x2_es2018: ES2018<'a, 'ctx>, + x2_es2017: ES2017<'a, 'ctx>, + x2_es2016: ES2016<'a, 'ctx>, + #[expect(unused)] + x3_es2015: ES2015<'a, 'ctx>, + x4_regexp: RegExp<'a, 'ctx>, + common: Common<'a, 'ctx>, +} + +impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> { + fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_program(program, ctx); + } + self.plugins.enter_program(program, ctx); + self.x1_jsx.enter_program(program, ctx); + self.x2_es2026.enter_program(program, ctx); + } + + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.decorator.exit_program(program, ctx); + self.x1_jsx.exit_program(program, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.exit_program(program, ctx); + } + self.x2_es2022.exit_program(program, ctx); + self.x2_es2020.exit_program(program, ctx); + self.x2_es2018.exit_program(program, ctx); + self.common.exit_program(program, ctx); + } + + // ALPHASORT + fn enter_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.common.enter_arrow_function_expression(arrow, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_arrow_function_expression(arrow, ctx); + } + self.x2_es2018.enter_arrow_function_expression(arrow, ctx); + } + + fn enter_variable_declaration( + &mut self, + decl: &mut VariableDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x2_es2018.enter_variable_declaration(decl, ctx); + } + + fn enter_variable_declarator( + &mut self, + decl: &mut VariableDeclarator<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_variable_declarator(decl, ctx); + } + self.plugins.enter_variable_declarator(decl, ctx); + } + + fn enter_big_int_literal(&mut self, node: &mut BigIntLiteral<'a>, ctx: &mut TraverseCtx<'a>) { + self.x2_es2020.enter_big_int_literal(node, ctx); + } + + fn enter_await_expression( + &mut self, + node: &mut AwaitExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x2_es2022.enter_await_expression(node, ctx); + } + + fn enter_import_specifier( + &mut self, + node: &mut ImportSpecifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x2_es2020.enter_import_specifier(node, ctx); + } + + fn enter_export_specifier( + &mut self, + node: &mut ExportSpecifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x2_es2020.enter_export_specifier(node, ctx); + } + + fn enter_binding_identifier( + &mut self, + node: &mut BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.common.enter_binding_identifier(node, ctx); + } + + fn enter_identifier_reference( + &mut self, + node: &mut IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.common.enter_identifier_reference(node, ctx); + } + + fn enter_binding_pattern(&mut self, pat: &mut BindingPattern<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_binding_pattern(pat, ctx); + } + } + + fn enter_call_expression(&mut self, expr: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_call_expression(expr, ctx); + } + self.plugins.enter_call_expression(expr, ctx); + self.x1_jsx.enter_call_expression(expr, ctx); + } + + fn enter_chain_element(&mut self, element: &mut ChainElement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_chain_element(element, ctx); + } + } + + fn enter_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.decorator.enter_class(class, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_class(class, ctx); + } + } + + fn exit_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.decorator.exit_class(class, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.exit_class(class, ctx); + } + self.x2_es2022.exit_class(class, ctx); + // `decorator` has some statements should be inserted after `class-properties` plugin. + self.decorator.exit_class_at_end(class, ctx); + } + + fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { + self.x2_es2022.enter_class_body(body, ctx); + } + + fn enter_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.enter_static_block(block, ctx); + self.x2_es2022.enter_static_block(block, ctx); + } + + fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.exit_static_block(block, ctx); + self.x2_es2026.exit_static_block(block, ctx); + self.x2_es2022.exit_static_block(block, ctx); + } + + #[inline] + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.enter_expression(expr, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_expression(expr, ctx); + } + self.plugins.enter_expression(expr, ctx); + self.x2_es2022.enter_expression(expr, ctx); + self.x2_es2021.enter_expression(expr, ctx); + self.x2_es2020.enter_expression(expr, ctx); + self.x2_es2018.enter_expression(expr, ctx); + self.x2_es2016.enter_expression(expr, ctx); + self.x4_regexp.enter_expression(expr, ctx); + } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.exit_expression(expr, ctx); + self.x1_jsx.exit_expression(expr, ctx); + self.x2_es2022.exit_expression(expr, ctx); + self.x2_es2018.exit_expression(expr, ctx); + self.x2_es2017.exit_expression(expr, ctx); + } + + fn enter_simple_assignment_target( + &mut self, + node: &mut SimpleAssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_simple_assignment_target(node, ctx); + } + } + + fn enter_assignment_target( + &mut self, + node: &mut AssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_assignment_target(node, ctx); + } + self.x2_es2022.enter_assignment_target(node, ctx); + } + + fn enter_formal_parameters( + &mut self, + node: &mut FormalParameters<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x2_es2020.enter_formal_parameters(node, ctx); + } + + fn exit_formal_parameters( + &mut self, + node: &mut FormalParameters<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x2_es2020.exit_formal_parameters(node, ctx); + } + + fn enter_formal_parameter( + &mut self, + param: &mut FormalParameter<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_formal_parameter(param, ctx); + } + } + + fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.enter_function(func, ctx); + self.x2_es2018.enter_function(func, ctx); + } + + fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.exit_function(func, ctx); + } + self.x1_jsx.exit_function(func, ctx); + self.x2_es2018.exit_function(func, ctx); + self.x2_es2017.exit_function(func, ctx); + self.common.exit_function(func, ctx); + } + + fn enter_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.enter_function_body(body, ctx); + self.x2_es2026.enter_function_body(body, ctx); + } + + fn exit_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.exit_function_body(body, ctx); + } + + fn enter_jsx_element(&mut self, node: &mut JSXElement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_jsx_element(node, ctx); + } + } + + fn enter_jsx_element_name(&mut self, node: &mut JSXElementName<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.enter_jsx_element_name(node, ctx); + } + + fn enter_jsx_member_expression_object( + &mut self, + node: &mut JSXMemberExpressionObject<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.common.enter_jsx_member_expression_object(node, ctx); + } + + fn enter_jsx_fragment(&mut self, node: &mut JSXFragment<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_jsx_fragment(node, ctx); + } + } + + fn enter_jsx_opening_element( + &mut self, + elem: &mut JSXOpeningElement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_jsx_opening_element(elem, ctx); + } + self.x1_jsx.enter_jsx_opening_element(elem, ctx); + } + + fn enter_method_definition( + &mut self, + def: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.decorator.enter_method_definition(def, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_method_definition(def, ctx); + } + } + + fn exit_method_definition( + &mut self, + def: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.decorator.exit_method_definition(def, ctx); + } + + fn enter_new_expression(&mut self, expr: &mut NewExpression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_new_expression(expr, ctx); + } + } + + fn enter_property_definition( + &mut self, + def: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.decorator.enter_property_definition(def, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_property_definition(def, ctx); + } + self.x2_es2022.enter_property_definition(def, ctx); + } + + fn exit_property_definition( + &mut self, + def: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.decorator.exit_property_definition(def, ctx); + self.x2_es2022.exit_property_definition(def, ctx); + } + + fn enter_accessor_property( + &mut self, + node: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.decorator.enter_accessor_property(node, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_accessor_property(node, ctx); + } + } + + fn exit_accessor_property( + &mut self, + node: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.decorator.exit_accessor_property(node, ctx); + } + + fn enter_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.common.enter_statements(stmts, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_statements(stmts, ctx); + } + } + + fn exit_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.common.exit_arrow_function_expression(arrow, ctx); + + // Some plugins may add new statements to the ArrowFunctionExpression's body, + // which can cause issues with the `() => x;` case, as it only allows a single statement. + // To address this, we wrap the last statement in a return statement and set the expression to false. + // This transforms the arrow function into the form `() => { return x; };`. + let statements = &mut arrow.body.statements; + if arrow.expression && statements.len() > 1 { + arrow.expression = false; + + // Reverse looping to find the expression statement, because other plugins could + // insert new statements after the expression statement. + // `() => x;` + // -> + // ``` + // () => { + // var new_insert_variable; + // return x; + // function new_insert_function() {} + // }; + // ``` + for stmt in statements.iter_mut().rev() { + let Statement::ExpressionStatement(expr_stmt) = stmt else { + continue; + }; + let expression = Some(expr_stmt.expression.take_in(ctx.ast)); + *stmt = ctx.ast.statement_return(SPAN, expression); + return; + } + unreachable!("At least one statement should be expression statement") + } + } + + fn exit_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.exit_statements(stmts, ctx); + } + self.common.exit_statements(stmts, ctx); + } + + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.exit_statement(stmt, ctx); + } + self.decorator.exit_statement(stmt, ctx); + self.x2_es2018.exit_statement(stmt, ctx); + self.x2_es2017.exit_statement(stmt, ctx); + } + + fn enter_tagged_template_expression( + &mut self, + expr: &mut TaggedTemplateExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_tagged_template_expression(expr, ctx); + } + } + + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + self.decorator.enter_statement(stmt, ctx); + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_statement(stmt, ctx); + } + self.x2_es2018.enter_statement(stmt, ctx); + self.x2_es2026.enter_statement(stmt, ctx); + } + + fn enter_declaration(&mut self, decl: &mut Declaration<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_declaration(decl, ctx); + } + } + + fn enter_if_statement(&mut self, stmt: &mut IfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_if_statement(stmt, ctx); + } + } + + fn enter_while_statement(&mut self, stmt: &mut WhileStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_while_statement(stmt, ctx); + } + } + + fn enter_do_while_statement( + &mut self, + stmt: &mut DoWhileStatement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_do_while_statement(stmt, ctx); + } + } + + fn enter_for_statement(&mut self, stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_for_statement(stmt, ctx); + } + } + + fn enter_for_of_statement(&mut self, stmt: &mut ForOfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_for_of_statement(stmt, ctx); + } + self.x2_es2026.enter_for_of_statement(stmt, ctx); + self.x2_es2018.enter_for_of_statement(stmt, ctx); + } + + fn enter_for_in_statement(&mut self, stmt: &mut ForInStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_for_in_statement(stmt, ctx); + } + self.x2_es2018.enter_for_in_statement(stmt, ctx); + } + + fn enter_try_statement(&mut self, stmt: &mut TryStatement<'a>, ctx: &mut TraverseCtx<'a>) { + self.x2_es2026.enter_try_statement(stmt, ctx); + } + + fn enter_catch_clause(&mut self, clause: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) { + self.x2_es2019.enter_catch_clause(clause, ctx); + self.x2_es2018.enter_catch_clause(clause, ctx); + } + + fn enter_import_declaration( + &mut self, + node: &mut ImportDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_import_declaration(node, ctx); + } + } + + fn enter_export_all_declaration( + &mut self, + node: &mut ExportAllDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_export_all_declaration(node, ctx); + } + self.x2_es2020.enter_export_all_declaration(node, ctx); + } + + fn enter_export_named_declaration( + &mut self, + node: &mut ExportNamedDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_export_named_declaration(node, ctx); + } + } + + fn enter_ts_export_assignment( + &mut self, + export_assignment: &mut TSExportAssignment<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_ts_export_assignment(export_assignment, ctx); + } + } + + fn enter_decorator( + &mut self, + node: &mut oxc_ast::ast::Decorator<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.decorator.enter_decorator(node, ctx); + } +} diff --git a/crates/swc_ecma_transformer/oxc/options/babel/env/mod.rs b/crates/swc_ecma_transformer/oxc/options/babel/env/mod.rs new file mode 100644 index 000000000000..e254e07ef95d --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/options/babel/env/mod.rs @@ -0,0 +1,70 @@ +use serde::Deserialize; + +use crate::{Module, options::EngineTargets}; + +fn default_as_true() -> bool { + true +} + +#[derive(Default, Debug, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct BabelEnvOptions { + #[serde(default)] + pub targets: EngineTargets, + + #[deprecated = "Not Implemented"] + #[serde(default = "default_as_true")] + pub bugfixes: bool, + + #[deprecated = "Not Implemented"] + pub spec: bool, + + #[deprecated = "Not Implemented"] + pub loose: bool, + + pub modules: Module, + + #[deprecated = "Not Implemented"] + pub debug: bool, + + #[deprecated = "Not Implemented"] + pub include: Option, + + #[deprecated = "Not Implemented"] + pub exclude: Option, + + #[deprecated = "Not Implemented"] + pub use_built_ins: Option, + + #[deprecated = "Not Implemented"] + pub corejs: Option, + + #[deprecated = "Not Implemented"] + pub force_all_transforms: bool, + + #[deprecated = "Not Implemented"] + pub config_path: Option, + + #[deprecated = "Not Implemented"] + pub ignore_browserslist_config: bool, + + #[deprecated = "Not Implemented"] + pub shipped_proposals: bool, +} + +#[derive(Default, Debug, Clone, Deserialize)] +pub enum BabelModule { + #[default] + #[serde(rename = "auto")] + Auto, + #[serde(rename = "amd")] + Amd, + #[serde(rename = "umd")] + Umd, + #[serde(rename = "systemjs")] + Systemjs, + #[serde(rename = "commonjs", alias = "cjs")] + Commonjs, + #[serde(untagged)] + Boolean(bool), +} diff --git a/crates/swc_ecma_transformer/oxc/options/babel/mod.rs b/crates/swc_ecma_transformer/oxc/options/babel/mod.rs new file mode 100644 index 000000000000..4fea2962e454 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/options/babel/mod.rs @@ -0,0 +1,192 @@ +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, de::DeserializeOwned}; + +use crate::CompilerAssumptions; + +mod env; +mod plugins; +mod presets; +pub use env::{BabelEnvOptions, BabelModule}; +pub use plugins::BabelPlugins; +pub use presets::BabelPresets; + +/// Babel options +/// +/// +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BabelOptions { + // Primary options + pub cwd: Option, + + // Config Loading options + + // Plugin and Preset options + #[serde(default)] + pub plugins: BabelPlugins, + + #[serde(default)] + pub presets: BabelPresets, + + // Misc options + pub source_type: Option, + + #[serde(default)] + pub assumptions: CompilerAssumptions, + + // Test options + pub throws: Option, + + #[serde(rename = "BABEL_8_BREAKING")] + pub babel_8_breaking: Option, + + /// Babel test helper for running tests on specific operating systems + pub os: Option>, + + // Parser options for babel-parser + #[serde(default)] + pub allow_return_outside_function: bool, + + #[serde(default)] + pub allow_await_outside_function: bool, + + #[serde(default)] + pub allow_undeclared_exports: bool, + + #[serde(default = "default_as_true")] + pub external_helpers: bool, +} + +/// +#[derive(Debug, Deserialize)] +struct PluginPresetEntries(Vec); + +/// +#[derive(Debug, Deserialize, Clone)] +#[serde(untagged)] +enum PluginPresetEntry { + String(String), + Vec1([String; 1]), + Tuple(String, serde_json::Value), + Triple(String, serde_json::Value, #[expect(unused)] String), +} + +impl PluginPresetEntry { + fn name(&self) -> &str { + match self { + Self::String(s) | Self::Tuple(s, _) | Self::Triple(s, _, _) => s, + Self::Vec1(s) => &s[0], + } + } + + fn value(self) -> Result { + match self { + Self::String(_) | Self::Vec1(_) => Ok(T::default()), + Self::Tuple(name, v) | Self::Triple(name, v, _) => { + serde_json::from_value::(v).map_err(|err| format!("{name}: {err}")) + } + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TestOs { + Linux, + Win32, + Windows, + Darwin, +} + +impl TestOs { + pub fn is_windows(&self) -> bool { + matches!(self, Self::Win32 | Self::Windows) + } +} + +fn default_as_true() -> bool { + true +} + +impl BabelOptions { + /// Read options.json and merge them with options.json from ancestors directories. + /// # Panics + pub fn from_test_path(path: &Path) -> Self { + let mut babel_options: Option = None; + let mut plugins_json = None; + let mut presets_json = None; + + for path in path.ancestors().take(3) { + let file = path.join("options.json"); + if !file.exists() { + continue; + } + + let content = std::fs::read_to_string(&file).unwrap(); + let mut new_value = serde_json::from_str::(&content).unwrap(); + + let new_plugins = new_value.as_object_mut().unwrap().remove("plugins"); + if plugins_json.is_none() { + plugins_json = new_plugins; + } + + let new_presets = new_value.as_object_mut().unwrap().remove("presets"); + if presets_json.is_none() { + presets_json = new_presets; + } + + let new_options: Self = serde_json::from_value::(new_value) + .unwrap_or_else(|err| panic!("{err:?}\n{}\n{content}", file.display())); + + if let Some(existing_options) = babel_options.as_mut() { + if existing_options.source_type.is_none() + && let Some(source_type) = new_options.source_type + { + existing_options.source_type = Some(source_type); + } + if existing_options.throws.is_none() + && let Some(throws) = new_options.throws + { + existing_options.throws = Some(throws); + } + } else { + babel_options = Some(new_options); + } + } + + let mut options = babel_options.unwrap_or_default(); + if let Some(plugins_json) = plugins_json { + options.plugins = serde_json::from_value::(plugins_json) + .unwrap_or_else(|err| panic!("{err:?}\n{}", path.display())); + } + if let Some(presets_json) = presets_json { + options.presets = serde_json::from_value::(presets_json) + .unwrap_or_else(|err| panic!("{err:?}\n{}", path.display())); + } + options + } + + pub fn is_jsx(&self) -> bool { + self.plugins.syntax_jsx + || self.presets.jsx.is_some() + || self.plugins.react_jsx.is_some() + || self.plugins.react_jsx_dev.is_some() + } + + pub fn is_typescript(&self) -> bool { + self.plugins.syntax_typescript.is_some() + } + + pub fn is_typescript_definition(&self) -> bool { + self.plugins.syntax_typescript.is_some_and(|o| o.dts) + } + + pub fn is_module(&self) -> bool { + self.source_type.as_ref().is_some_and(|s| s.as_str() == "module") + } + + pub fn is_unambiguous(&self) -> bool { + self.source_type.as_ref().is_some_and(|s| s.as_str() == "unambiguous") + } +} diff --git a/crates/swc_ecma_transformer/oxc/options/babel/plugins.rs b/crates/swc_ecma_transformer/oxc/options/babel/plugins.rs new file mode 100644 index 000000000000..b47a359a5fa7 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/options/babel/plugins.rs @@ -0,0 +1,180 @@ +use serde::Deserialize; + +use crate::{ + DecoratorOptions, TypeScriptOptions, es2015::ArrowFunctionsOptions, + es2018::ObjectRestSpreadOptions, es2022::ClassPropertiesOptions, jsx::JsxOptions, + plugins::StyledComponentsOptions, +}; + +use super::PluginPresetEntries; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +pub struct SyntaxTypeScriptOptions { + #[serde(default)] + pub dts: bool, +} + +#[derive(Debug, Default, Clone, Deserialize)] +pub struct SyntaxDecoratorOptions { + #[serde(default)] + pub version: String, +} + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(try_from = "PluginPresetEntries")] +pub struct BabelPlugins { + pub errors: Vec, + pub unsupported: Vec, + // syntax + pub syntax_typescript: Option, + pub syntax_jsx: bool, + // decorators + pub syntax_decorators: Option, + pub proposal_decorators: Option, + // ts + pub typescript: Option, + // jsx + pub react_jsx: Option, + pub react_jsx_dev: Option, + pub react_jsx_self: bool, + pub react_jsx_source: bool, + pub react_display_name: bool, + // modules + pub modules_commonjs: bool, + // regexp + pub sticky_flag: bool, + pub unicode_flag: bool, + pub dot_all_flag: bool, + pub look_behind_assertions: bool, + pub named_capture_groups: bool, + pub unicode_property_escapes: bool, + pub match_indices: bool, + /// Enables plugin to transform the RegExp literal has `v` flag + pub set_notation: bool, + // ES2015 + pub arrow_function: Option, + // ES2016 + pub exponentiation_operator: bool, + // ES2017 + pub async_to_generator: bool, + // ES2018 + pub object_rest_spread: Option, + pub async_generator_functions: bool, + // ES2019 + pub optional_catch_binding: bool, + // ES2020 + pub export_namespace_from: bool, + pub optional_chaining: bool, + pub nullish_coalescing_operator: bool, + // ES2021 + pub logical_assignment_operators: bool, + // ES2022 + pub class_static_block: bool, + pub class_properties: Option, + // ES2026 + pub explicit_resource_management: bool, + // Decorator + pub legacy_decorator: Option, + // Built-in plugins + pub styled_components: Option, +} + +impl TryFrom for BabelPlugins { + type Error = String; + + fn try_from(entries: PluginPresetEntries) -> Result { + let mut p = Self::default(); + for entry in entries.0 { + match entry.name() { + "typescript" | "syntax-typescript" => { + p.syntax_typescript = Some(entry.value::()?); + } + "jsx" | "syntax-jsx" => p.syntax_jsx = true, + "syntax-decorators" => { + p.syntax_decorators = Some(entry.value::()?); + } + "proposal-decorators" => { + p.proposal_decorators = Some(entry.value::()?); + } + "transform-typescript" => { + p.typescript = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "transform-react-jsx" => { + #[derive(Deserialize, Default)] + struct Pure { + pure: bool, + } + + let pure = entry.clone().value::().map(|p| p.pure).unwrap_or(false); + p.react_jsx = entry + .value::() + .map_err(|err| p.errors.push(err)) + .map(|mut options| { + // `pure` only defaults to `true` in `preset-react` + options.pure = pure; + options + }) + .ok(); + } + "transform-react-jsx-development" => { + p.react_jsx_dev = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "transform-react-display-name" => p.react_display_name = true, + "transform-react-jsx-self" => p.react_jsx_self = true, + "transform-react-jsx-source" => p.react_jsx_source = true, + "transform-modules-commonjs" => p.modules_commonjs = true, + "transform-sticky-regex" => p.sticky_flag = true, + "transform-unicode-regex" => p.unicode_flag = true, + "transform-dotall-regex" => p.dot_all_flag = true, + "esbuild-regexp-lookbehind-assertions" => p.look_behind_assertions = true, + "transform-named-capturing-groups-regex" => p.named_capture_groups = true, + "transform-unicode-property-regex" => p.unicode_property_escapes = true, + "esbuild-regexp-match-indices" => p.match_indices = true, + "transform-unicode-sets-regex" => p.set_notation = true, + "transform-arrow-functions" => { + p.arrow_function = entry + .value::() + .map_err(|err| p.errors.push(err)) + .ok(); + } + "transform-exponentiation-operator" => p.exponentiation_operator = true, + "transform-async-to-generator" => p.async_to_generator = true, + "transform-object-rest-spread" => { + p.object_rest_spread = entry + .value::() + .map_err(|err| p.errors.push(err)) + .ok(); + } + "transform-async-generator-functions" => p.async_generator_functions = true, + "transform-optional-catch-binding" => p.optional_catch_binding = true, + "transform-export-namespace-from" => p.export_namespace_from = true, + "transform-optional-chaining" => p.optional_chaining = true, + "transform-nullish-coalescing-operator" => p.nullish_coalescing_operator = true, + "transform-logical-assignment-operators" => p.logical_assignment_operators = true, + "transform-class-static-block" => p.class_static_block = true, + "transform-class-properties" => { + p.class_properties = entry + .value::() + .map_err(|err| p.errors.push(err)) + .ok(); + } + // This is not a Babel plugin, we pretend it exists for running legacy decorator by Babel options + "transform-legacy-decorator" => { + p.legacy_decorator = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "transform-explicit-resource-management" => p.explicit_resource_management = true, + "styled-components" => { + p.styled_components = entry + .value::() + .map_err(|err| p.errors.push(err)) + .ok(); + } + s => p.unsupported.push(s.to_string()), + } + } + Ok(p) + } +} diff --git a/crates/swc_ecma_transformer/oxc/options/babel/presets.rs b/crates/swc_ecma_transformer/oxc/options/babel/presets.rs new file mode 100644 index 000000000000..40c4dfe24a94 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/options/babel/presets.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; + +use crate::{EnvOptions, JsxOptions, TypeScriptOptions}; + +use super::PluginPresetEntries; + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(try_from = "PluginPresetEntries")] +pub struct BabelPresets { + pub errors: Vec, + pub unsupported: Vec, + + pub env: Option, + + pub jsx: Option, + + pub typescript: Option, +} + +impl TryFrom for BabelPresets { + type Error = String; + + fn try_from(entries: PluginPresetEntries) -> Result { + let mut p = Self::default(); + for entry in entries.0 { + match entry.name() { + "env" => { + p.env = entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "typescript" => { + p.typescript = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "react" => { + p.jsx = entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + s => p.unsupported.push(s.to_string()), + } + } + Ok(p) + } +} diff --git a/crates/swc_ecma_transformer/oxc/options/env.rs b/crates/swc_ecma_transformer/oxc/options/env.rs new file mode 100644 index 000000000000..324ba6d4de81 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/options/env.rs @@ -0,0 +1,181 @@ +use serde::Deserialize; + +use crate::{ + es2015::{ArrowFunctionsOptions, ES2015Options}, + es2016::ES2016Options, + es2017::ES2017Options, + es2018::{ES2018Options, ObjectRestSpreadOptions}, + es2019::ES2019Options, + es2020::ES2020Options, + es2021::ES2021Options, + es2022::{ClassPropertiesOptions, ES2022Options}, + es2026::ES2026Options, + regexp::RegExpOptions, +}; + +use super::{Module, babel::BabelEnvOptions}; +use oxc_compat::{ESFeature, EngineTargets}; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(try_from = "BabelEnvOptions")] +pub struct EnvOptions { + /// Specify what module code is generated. + pub module: Module, + + pub regexp: RegExpOptions, + + pub es2015: ES2015Options, + + pub es2016: ES2016Options, + + pub es2017: ES2017Options, + + pub es2018: ES2018Options, + + pub es2019: ES2019Options, + + pub es2020: ES2020Options, + + pub es2021: ES2021Options, + + pub es2022: ES2022Options, + + pub es2026: ES2026Options, +} + +impl EnvOptions { + /// Explicitly enable all plugins that are ready, mainly for testing purposes. + /// + /// NOTE: for internal use only + #[doc(hidden)] + pub fn enable_all(include_unfinished_plugins: bool) -> Self { + Self { + module: Module::default(), + regexp: RegExpOptions { + sticky_flag: true, + unicode_flag: true, + unicode_property_escapes: true, + dot_all_flag: true, + named_capture_groups: true, + look_behind_assertions: true, + match_indices: true, + set_notation: true, + }, + es2015: ES2015Options { + // Turned off because it is not ready. + arrow_function: if include_unfinished_plugins { + Some(ArrowFunctionsOptions::default()) + } else { + None + }, + }, + es2016: ES2016Options { exponentiation_operator: true }, + es2017: ES2017Options { async_to_generator: true }, + es2018: ES2018Options { + object_rest_spread: Some(ObjectRestSpreadOptions::default()), + async_generator_functions: true, + }, + es2019: ES2019Options { optional_catch_binding: true }, + es2020: ES2020Options { + export_namespace_from: true, + nullish_coalescing_operator: true, + // Turn this on would throw error for all bigints. + big_int: false, + optional_chaining: true, + arbitrary_module_namespace_names: false, + }, + es2021: ES2021Options { logical_assignment_operators: true }, + es2022: ES2022Options { + class_static_block: true, + class_properties: Some(ClassPropertiesOptions::default()), + // Turn this on would throw error for all top-level awaits. + top_level_await: false, + }, + es2026: ES2026Options { explicit_resource_management: true }, + } + } + + /// Initialize from a [browserslist] query. + /// + /// # Errors + /// + /// * When the query failed to parse. + /// + /// [browserslist]: + pub fn from_browserslist_query(query: &str) -> Result { + EngineTargets::try_from_query(query).map(Self::from) + } + + /// # Errors + /// + /// * When the query failed to parse. + pub fn from_target(s: &str) -> Result { + EngineTargets::from_target(s).map(Self::from) + } + + /// # Errors + /// + /// * When the query failed to parse. + pub fn from_target_list>(list: &[S]) -> Result { + EngineTargets::from_target_list(list).map(Self::from) + } +} + +impl From for EnvOptions { + fn from(o: BabelEnvOptions) -> Self { + Self::from(o.targets) + } +} + +impl From for EnvOptions { + fn from(o: EngineTargets) -> Self { + #[allow(clippy::enum_glob_use, clippy::allow_attributes)] + use ESFeature::*; + Self { + module: Module::default(), + regexp: RegExpOptions { + sticky_flag: o.has_feature(ES2015StickyRegex), + unicode_flag: o.has_feature(ES2015UnicodeRegex), + unicode_property_escapes: o.has_feature(ES2018UnicodePropertyRegex), + dot_all_flag: o.has_feature(ES2018DotallRegex), + named_capture_groups: o.has_feature(ES2018NamedCapturingGroupsRegex), + look_behind_assertions: o.has_feature(ES2018LookbehindRegex), + match_indices: o.has_feature(ES2022MatchIndicesRegex), + set_notation: o.has_feature(ES2024UnicodeSetsRegex), + }, + es2015: ES2015Options { + arrow_function: o.has_feature(ES2015ArrowFunctions).then(Default::default), + }, + es2016: ES2016Options { + exponentiation_operator: o.has_feature(ES2016ExponentiationOperator), + }, + es2017: ES2017Options { async_to_generator: o.has_feature(ES2017AsyncToGenerator) }, + es2018: ES2018Options { + object_rest_spread: o.has_feature(ES2018ObjectRestSpread).then(Default::default), + async_generator_functions: o.has_feature(ES2018AsyncGeneratorFunctions), + }, + es2019: ES2019Options { + optional_catch_binding: o.has_feature(ES2019OptionalCatchBinding), + }, + es2020: ES2020Options { + export_namespace_from: o.has_feature(ES2020ExportNamespaceFrom), + nullish_coalescing_operator: o.has_feature(ES2020NullishCoalescingOperator), + big_int: o.has_feature(ES2020BigInt), + optional_chaining: o.has_feature(ES2020OptionalChaining), + arbitrary_module_namespace_names: o + .has_feature(ES2020ArbitraryModuleNamespaceNames), + }, + es2021: ES2021Options { + logical_assignment_operators: o.has_feature(ES2021LogicalAssignmentOperators), + }, + es2022: ES2022Options { + class_static_block: o.has_feature(ES2022ClassStaticBlock), + class_properties: o.has_feature(ES2022ClassProperties).then(Default::default), + top_level_await: o.has_feature(ES2022TopLevelAwait), + }, + es2026: ES2026Options { + explicit_resource_management: o.has_feature(ES2026ExplicitResourceManagement), + }, + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/options/mod.rs b/crates/swc_ecma_transformer/oxc/options/mod.rs new file mode 100644 index 000000000000..a3e461fe41c1 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/options/mod.rs @@ -0,0 +1,294 @@ +use std::path::PathBuf; + +use crate::{ + ReactRefreshOptions, + common::helper_loader::{HelperLoaderMode, HelperLoaderOptions}, + compiler_assumptions::CompilerAssumptions, + decorator::DecoratorOptions, + es2015::ES2015Options, + es2016::ES2016Options, + es2017::ES2017Options, + es2018::ES2018Options, + es2019::ES2019Options, + es2020::ES2020Options, + es2021::ES2021Options, + es2022::ES2022Options, + es2026::ES2026Options, + jsx::JsxOptions, + plugins::{PluginsOptions, StyledComponentsOptions}, + proposals::ProposalOptions, + regexp::RegExpOptions, + typescript::TypeScriptOptions, +}; + +pub mod babel; +mod env; +mod module; + +use babel::BabelOptions; +pub use env::EnvOptions; +pub use module::Module; +pub use oxc_compat::{Engine, EngineTargets}; +pub use oxc_syntax::es_target::ESTarget; + +/// +#[derive(Debug, Default, Clone)] +pub struct TransformOptions { + // + // Primary Options + // + /// The working directory that all paths in the programmatic options will be resolved relative to. + pub cwd: PathBuf, + + // Core + /// Set assumptions in order to produce smaller output. + /// For more information, check the [assumptions](https://babel.dev/docs/assumptions) documentation page. + pub assumptions: CompilerAssumptions, + + // Plugins + /// [preset-typescript](https://babeljs.io/docs/babel-preset-typescript) + pub typescript: TypeScriptOptions, + + /// Decorator + pub decorator: DecoratorOptions, + + /// Jsx Transform + /// + /// See [preset-react](https://babeljs.io/docs/babel-preset-react) + pub jsx: JsxOptions, + + /// ECMAScript Env Options + pub env: EnvOptions, + + /// Proposals + pub proposals: ProposalOptions, + + /// Plugins + pub plugins: PluginsOptions, + + pub helper_loader: HelperLoaderOptions, +} + +impl TransformOptions { + /// Explicitly enable all plugins that are ready, mainly for testing purposes. + /// + /// NOTE: for internal use only + #[doc(hidden)] + pub fn enable_all() -> Self { + Self { + cwd: PathBuf::new(), + assumptions: CompilerAssumptions::default(), + typescript: TypeScriptOptions::default(), + decorator: DecoratorOptions { legacy: true, emit_decorator_metadata: true }, + jsx: JsxOptions { + development: true, + refresh: Some(ReactRefreshOptions::default()), + ..JsxOptions::default() + }, + env: EnvOptions::enable_all(/* include_unfinished_plugins */ false), + proposals: ProposalOptions::default(), + plugins: PluginsOptions { styled_components: Some(StyledComponentsOptions::default()) }, + helper_loader: HelperLoaderOptions { + mode: HelperLoaderMode::Runtime, + ..Default::default() + }, + } + } + + /// Initialize from a comma separated list of `target`s and `environmens`s. + /// + /// e.g. `es2022,chrome58,edge16`. + /// + /// # Errors + /// + /// * Same targets specified multiple times. + /// * No matching target. + /// * Invalid version. + pub fn from_target(s: &str) -> Result { + EnvOptions::from_target(s).map(|env| Self { env, ..Self::default() }) + } + + /// Initialize from a list of `target`s and `environmens`s. + /// + /// e.g. `["es2020", "chrome58", "edge16", "firefox57", "node12", "safari11"]`. + /// + /// `target`: `es5`, `es2015` ... `es2024`, `esnext`. + /// `environment`: `chrome`, `deno`, `edge`, `firefox`, `hermes`, `ie`, `ios`, `node`, `opera`, `rhino`, `safari` + /// + /// + /// + /// # Errors + /// + /// * Same targets specified multiple times. + /// * No matching target. + /// * Invalid version. + pub fn from_target_list>(list: &[S]) -> Result { + EnvOptions::from_target_list(list).map(|env| Self { env, ..Self::default() }) + } +} + +impl From for TransformOptions { + fn from(target: ESTarget) -> Self { + use oxc_compat::ESVersion; + let mut engine_targets = EngineTargets::default(); + engine_targets.insert(Engine::Es, target.version()); + let env = EnvOptions::from(engine_targets); + Self { env, ..Self::default() } + } +} + +impl TryFrom<&BabelOptions> for TransformOptions { + type Error = Vec; + + /// If the `options` contains any unknown fields, they will be returned as a list of errors. + fn try_from(options: &BabelOptions) -> Result { + let mut errors = Vec::::new(); + errors.extend(options.plugins.errors.iter().map(Clone::clone)); + errors.extend(options.presets.errors.iter().map(Clone::clone)); + + let typescript = options + .presets + .typescript + .clone() + .or_else(|| options.plugins.typescript.clone()) + .unwrap_or_default(); + + let decorator = DecoratorOptions { + legacy: options.plugins.legacy_decorator.is_some(), + emit_decorator_metadata: options + .plugins + .legacy_decorator + .is_some_and(|o| o.emit_decorator_metadata), + }; + + let jsx = if let Some(options) = &options.presets.jsx { + options.clone() + } else { + let mut jsx_options = if let Some(options) = &options.plugins.react_jsx_dev { + options.clone() + } else if let Some(options) = &options.plugins.react_jsx { + options.clone() + } else { + JsxOptions::default() + }; + jsx_options.development = options.plugins.react_jsx_dev.is_some(); + jsx_options.jsx_plugin = options.plugins.react_jsx.is_some(); + jsx_options.display_name_plugin = options.plugins.react_display_name; + jsx_options.jsx_self_plugin = options.plugins.react_jsx_self; + jsx_options.jsx_source_plugin = options.plugins.react_jsx_source; + jsx_options + }; + + let env = options.presets.env.unwrap_or_default(); + + let module = Module::try_from(&options.plugins).unwrap_or_else(|_| { + options.presets.env.as_ref().map(|env| env.module).unwrap_or_default() + }); + + let regexp = RegExpOptions { + sticky_flag: env.regexp.sticky_flag || options.plugins.sticky_flag, + unicode_flag: env.regexp.unicode_flag || options.plugins.unicode_flag, + dot_all_flag: env.regexp.dot_all_flag || options.plugins.dot_all_flag, + look_behind_assertions: env.regexp.look_behind_assertions + || options.plugins.look_behind_assertions, + named_capture_groups: env.regexp.named_capture_groups + || options.plugins.named_capture_groups, + unicode_property_escapes: env.regexp.unicode_property_escapes + || options.plugins.unicode_property_escapes, + match_indices: env.regexp.match_indices, + set_notation: env.regexp.set_notation || options.plugins.set_notation, + }; + + let es2015 = ES2015Options { + arrow_function: options.plugins.arrow_function.or(env.es2015.arrow_function), + }; + + let es2016 = ES2016Options { + exponentiation_operator: options.plugins.exponentiation_operator + || env.es2016.exponentiation_operator, + }; + + let es2017 = ES2017Options { + async_to_generator: options.plugins.async_to_generator || env.es2017.async_to_generator, + }; + + let es2018 = ES2018Options { + object_rest_spread: options + .plugins + .object_rest_spread + .or(env.es2018.object_rest_spread), + async_generator_functions: options.plugins.async_generator_functions + || env.es2018.async_generator_functions, + }; + + let es2019 = ES2019Options { + optional_catch_binding: options.plugins.optional_catch_binding + || env.es2019.optional_catch_binding, + }; + + let es2020 = ES2020Options { + export_namespace_from: options.plugins.export_namespace_from + || env.es2020.export_namespace_from, + optional_chaining: options.plugins.optional_chaining || env.es2020.optional_chaining, + nullish_coalescing_operator: options.plugins.nullish_coalescing_operator + || env.es2020.nullish_coalescing_operator, + big_int: env.es2020.big_int, + arbitrary_module_namespace_names: env.es2020.arbitrary_module_namespace_names, + }; + + let es2021 = ES2021Options { + logical_assignment_operators: options.plugins.logical_assignment_operators + || env.es2021.logical_assignment_operators, + }; + + let es2022 = ES2022Options { + class_static_block: options.plugins.class_static_block || env.es2022.class_static_block, + class_properties: options.plugins.class_properties.or(env.es2022.class_properties), + top_level_await: env.es2022.top_level_await, + }; + + if !errors.is_empty() { + return Err(errors); + } + + let helper_loader = HelperLoaderOptions { + mode: if options.external_helpers { + HelperLoaderMode::External + } else { + HelperLoaderMode::default() + }, + ..HelperLoaderOptions::default() + }; + + let mut plugins = PluginsOptions::default(); + if let Some(styled_components) = &options.plugins.styled_components { + plugins.styled_components = Some(styled_components.clone()); + } + + Ok(Self { + cwd: options.cwd.clone().unwrap_or_default(), + assumptions: options.assumptions, + typescript, + decorator, + jsx, + env: EnvOptions { + module, + regexp, + es2015, + es2016, + es2017, + es2018, + es2019, + es2020, + es2021, + es2022, + es2026: ES2026Options { + explicit_resource_management: options.plugins.explicit_resource_management, + }, + }, + proposals: ProposalOptions::default(), + helper_loader, + plugins, + }) + } +} diff --git a/crates/swc_ecma_transformer/oxc/options/module.rs b/crates/swc_ecma_transformer/oxc/options/module.rs new file mode 100644 index 000000000000..6a8ca0a2d7a7 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/options/module.rs @@ -0,0 +1,57 @@ +use oxc_diagnostics::Error; +use serde::Deserialize; + +use super::babel::BabelPlugins; +use crate::options::babel::BabelModule; + +/// Specify what module code is generated. +/// +/// References: +/// - esbuild: +/// - Babel: +/// - TypeScript: +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(try_from = "BabelModule")] +#[non_exhaustive] +pub enum Module { + #[default] + Preserve, + Esm, + CommonJS, +} + +impl Module { + /// Check if the module is ECMAScript Module(ESM). + pub fn is_esm(self) -> bool { + matches!(self, Self::Esm) + } + + /// Check if the module is CommonJS. + pub fn is_commonjs(self) -> bool { + matches!(self, Self::CommonJS) + } +} + +impl TryFrom for Module { + type Error = Error; + + fn try_from(value: BabelModule) -> Result { + match value { + BabelModule::Commonjs => Ok(Self::CommonJS), + BabelModule::Auto | BabelModule::Boolean(false) => Ok(Self::Preserve), + _ => Err(Error::msg(format!("{value:?} module is not implemented."))), + } + } +} + +impl TryFrom<&BabelPlugins> for Module { + type Error = Error; + + fn try_from(value: &BabelPlugins) -> Result { + if value.modules_commonjs { + Ok(Self::CommonJS) + } else { + Err(Error::msg("Doesn't find any transform-modules-* plugin.")) + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/state.rs b/crates/swc_ecma_transformer/oxc/state.rs new file mode 100644 index 000000000000..eb7110ef1abb --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/state.rs @@ -0,0 +1,6 @@ +use std::marker::PhantomData; + +#[derive(Default)] +pub struct TransformState<'a> { + data: PhantomData<&'a ()>, +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/annotations.rs b/crates/swc_ecma_transformer/oxc/typescript/annotations.rs new file mode 100644 index 000000000000..20c09753686a --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/annotations.rs @@ -0,0 +1,599 @@ +use oxc_allocator::{TakeIn, Vec as ArenaVec}; +use oxc_ast::ast::*; +use oxc_diagnostics::OxcDiagnostic; +use oxc_semantic::SymbolFlags; +use oxc_span::{Atom, GetSpan, SPAN, Span}; +use oxc_syntax::{ + operator::AssignmentOperator, + reference::ReferenceFlags, + scope::{ScopeFlags, ScopeId}, + symbol::SymbolId, +}; +use oxc_traverse::Traverse; + +use crate::{ + TypeScriptOptions, + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct TypeScriptAnnotations<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + + // Options + only_remove_type_imports: bool, + + /// Assignments to be added to the constructor body + assignments: Vec>, + has_super_call: bool, + + has_jsx_element: bool, + has_jsx_fragment: bool, + jsx_element_import_name: String, + jsx_fragment_import_name: String, +} + +impl<'a, 'ctx> TypeScriptAnnotations<'a, 'ctx> { + pub fn new(options: &TypeScriptOptions, ctx: &'ctx TransformCtx<'a>) -> Self { + let jsx_element_import_name = if options.jsx_pragma.contains('.') { + options.jsx_pragma.split('.').next().map(String::from).unwrap() + } else { + options.jsx_pragma.to_string() + }; + + let jsx_fragment_import_name = if options.jsx_pragma_frag.contains('.') { + options.jsx_pragma_frag.split('.').next().map(String::from).unwrap() + } else { + options.jsx_pragma_frag.to_string() + }; + + Self { + ctx, + only_remove_type_imports: options.only_remove_type_imports, + has_super_call: false, + assignments: vec![], + has_jsx_element: false, + has_jsx_fragment: false, + jsx_element_import_name, + jsx_fragment_import_name, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptAnnotations<'a, '_> { + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + let mut no_modules_remaining = true; + let mut some_modules_deleted = false; + + program.body.retain_mut(|stmt| { + let need_retain = match stmt { + Statement::ExportNamedDeclaration(decl) if decl.declaration.is_some() => { + decl.declaration.as_ref().is_some_and(|decl| !decl.is_typescript_syntax()) + } + Statement::ExportNamedDeclaration(decl) => { + if decl.export_kind.is_type() { + false + } else if decl.specifiers.is_empty() { + // `export {}` or `export {} from 'mod'` + // Keep the export declaration if there are no export specifiers + true + } else { + decl.specifiers + .retain(|specifier| Self::can_retain_export_specifier(specifier, ctx)); + // Keep the export declaration if there are still specifiers after removing type exports + !decl.specifiers.is_empty() + } + } + Statement::ExportAllDeclaration(decl) => !decl.export_kind.is_type(), + Statement::ExportDefaultDeclaration(decl) => { + !decl.is_typescript_syntax() + && !matches!( + &decl.declaration, + ExportDefaultDeclarationKind::Identifier(ident) if Self::is_refers_to_type(ident, ctx) + ) + } + Statement::ImportDeclaration(decl) => { + if decl.import_kind.is_type() { + false + } else if let Some(specifiers) = &mut decl.specifiers { + if specifiers.is_empty() { + // import {} from 'mod' -> import 'mod' + decl.specifiers = None; + true + } else { + specifiers.retain(|specifier| { + let id = match specifier { + ImportDeclarationSpecifier::ImportSpecifier(s) => { + if s.import_kind.is_type() { + return false; + } + &s.local + } + ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { + &s.local + } + ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => { + &s.local + } + }; + // If `only_remove_type_imports` is true, then we can return `true` to keep it because + // it is not a type import, otherwise we need to check if the identifier is referenced + if self.only_remove_type_imports { + true + } else { + self.has_value_reference(id, ctx) + } + }); + + if specifiers.is_empty() { + // `import { type A } from 'mod'` + if self.only_remove_type_imports { + // -> `import 'mod'` + decl.specifiers = None; + true + } else { + // Remove the import declaration if all specifiers are removed + false + } + } else { + true + } + } + } else { + true + } + } + // `import Binding = X.Y.Z` + // `Binding` can be referenced as a value or a type, but here we already know it only as a type + // See `TypeScriptModule::transform_ts_import_equals` + Statement::TSTypeAliasDeclaration(_) + | Statement::TSExportAssignment(_) + | Statement::TSNamespaceExportDeclaration(_) => false, + _ => return true, + }; + + if need_retain { + no_modules_remaining = false; + } else { + some_modules_deleted = true; + } + + need_retain + }); + + // Determine if we still have import/export statements, otherwise we + // need to inject an empty statement (`export {}`) so that the file is + // still considered a module + if no_modules_remaining && some_modules_deleted && self.ctx.module_imports.is_empty() { + let export_decl = Statement::ExportNamedDeclaration( + ctx.ast.plain_export_named_declaration(SPAN, ctx.ast.vec(), None), + ); + program.body.push(export_decl); + } + } + + fn enter_arrow_function_expression( + &mut self, + expr: &mut ArrowFunctionExpression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + expr.type_parameters = None; + expr.return_type = None; + } + + fn enter_variable_declarator( + &mut self, + decl: &mut VariableDeclarator<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + decl.definite = false; + } + + fn enter_binding_pattern(&mut self, pat: &mut BindingPattern<'a>, _ctx: &mut TraverseCtx<'a>) { + pat.type_annotation = None; + + if pat.kind.is_binding_identifier() { + pat.optional = false; + } + } + + fn enter_call_expression(&mut self, expr: &mut CallExpression<'a>, _ctx: &mut TraverseCtx<'a>) { + expr.type_arguments = None; + } + + fn enter_chain_element(&mut self, element: &mut ChainElement<'a>, ctx: &mut TraverseCtx<'a>) { + if let ChainElement::TSNonNullExpression(e) = element { + *element = match e.expression.get_inner_expression_mut().take_in(ctx.ast) { + Expression::CallExpression(call_expr) => ChainElement::CallExpression(call_expr), + expr @ match_member_expression!(Expression) => { + ChainElement::from(expr.into_member_expression()) + } + _ => { + /* syntax error */ + return; + } + } + } + } + + fn enter_class(&mut self, class: &mut Class<'a>, _ctx: &mut TraverseCtx<'a>) { + class.type_parameters = None; + class.super_type_arguments = None; + class.implements.clear(); + class.r#abstract = false; + + // Remove type only members + class.body.body.retain(|elem| match elem { + ClassElement::MethodDefinition(method) => { + matches!(method.r#type, MethodDefinitionType::MethodDefinition) + && !method.value.is_typescript_syntax() + } + ClassElement::PropertyDefinition(prop) => { + matches!(prop.r#type, PropertyDefinitionType::PropertyDefinition) + } + ClassElement::AccessorProperty(prop) => { + matches!(prop.r#type, AccessorPropertyType::AccessorProperty) + } + ClassElement::TSIndexSignature(_) => false, + ClassElement::StaticBlock(_) => true, + }); + } + + fn exit_class(&mut self, class: &mut Class<'a>, _: &mut TraverseCtx<'a>) { + // Remove `declare` properties from the class body, other ts-only properties have been removed in `enter_class`. + // The reason that removing `declare` properties here because the legacy-decorator plugin needs to transform + // `declare` field in the `exit_class` phase, so we have to ensure this step is run after the legacy-decorator plugin. + class + .body + .body + .retain(|elem| !matches!(elem, ClassElement::PropertyDefinition(prop) if prop.declare)); + } + + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if expr.is_typescript_syntax() { + let inner_expr = expr.get_inner_expression_mut(); + *expr = inner_expr.take_in(ctx.ast); + } + } + + fn enter_simple_assignment_target( + &mut self, + target: &mut SimpleAssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(expr) = target.get_expression_mut() { + match expr.get_inner_expression_mut() { + // `foo!++` to `foo++` + inner_expr @ Expression::Identifier(_) => { + let inner_expr = inner_expr.take_in(ctx.ast); + let Expression::Identifier(ident) = inner_expr else { + unreachable!(); + }; + *target = SimpleAssignmentTarget::AssignmentTargetIdentifier(ident); + } + // `foo.bar!++` to `foo.bar++` + inner_expr @ match_member_expression!(Expression) => { + let inner_expr = inner_expr.take_in(ctx.ast); + let member_expr = inner_expr.into_member_expression(); + *target = SimpleAssignmentTarget::from(member_expr); + } + _ => { + // This should be never hit until more syntax is added to the JavaScript/TypeScrips + self.ctx.error(OxcDiagnostic::error("Cannot strip out typescript syntax if SimpleAssignmentTarget is not an IdentifierReference or MemberExpression")); + } + } + } + } + + fn enter_assignment_target( + &mut self, + target: &mut AssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(expr) = target.get_expression_mut() { + let inner_expr = expr.get_inner_expression_mut(); + if inner_expr.is_member_expression() { + let inner_expr = inner_expr.take_in(ctx.ast); + let member_expr = inner_expr.into_member_expression(); + *target = AssignmentTarget::from(member_expr); + } + } + } + + fn enter_formal_parameter( + &mut self, + param: &mut FormalParameter<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + param.accessibility = None; + param.readonly = false; + param.r#override = false; + } + + fn exit_function(&mut self, func: &mut Function<'a>, _ctx: &mut TraverseCtx<'a>) { + func.this_param = None; + func.type_parameters = None; + func.return_type = None; + } + + fn enter_jsx_opening_element( + &mut self, + elem: &mut JSXOpeningElement<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + elem.type_arguments = None; + } + + fn enter_method_definition( + &mut self, + def: &mut MethodDefinition<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + def.accessibility = None; + def.optional = false; + def.r#override = false; + } + + fn enter_new_expression(&mut self, expr: &mut NewExpression<'a>, _ctx: &mut TraverseCtx<'a>) { + expr.type_arguments = None; + } + + fn enter_property_definition( + &mut self, + def: &mut PropertyDefinition<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + def.accessibility = None; + def.definite = false; + def.r#override = false; + def.optional = false; + def.readonly = false; + def.type_annotation = None; + } + + fn enter_accessor_property( + &mut self, + def: &mut AccessorProperty<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + def.accessibility = None; + def.definite = false; + def.type_annotation = None; + } + + fn enter_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + _ctx: &mut TraverseCtx<'a>, + ) { + // Remove declare declaration + stmts.retain( + |stmt| { + if let Some(decl) = stmt.as_declaration() { !decl.declare() } else { true } + }, + ); + } + + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + // Add assignments after super calls + if self.assignments.is_empty() { + return; + } + + let has_super_call = matches!(stmt, Statement::ExpressionStatement(stmt) if stmt.expression.is_super_call_expression()); + if !has_super_call { + return; + } + + // Add assignments after super calls + self.ctx.statement_injector.insert_many_after( + stmt, + self.assignments + .iter() + .map(|assignment| assignment.create_this_property_assignment(ctx)), + ); + self.has_super_call = true; + } + + fn exit_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + _ctx: &mut TraverseCtx<'a>, + ) { + // Remove TS specific statements + stmts.retain(|stmt| match stmt { + Statement::ExpressionStatement(s) => !s.expression.is_typescript_syntax(), + match_declaration!(Statement) => !stmt.to_declaration().is_typescript_syntax(), + // Ignore ModuleDeclaration as it's handled in the program + _ => true, + }); + } + + /// Transform if statement's consequent and alternate to block statements if they are super calls + /// ```ts + /// if (true) super() else super(); + /// // to + /// if (true) { super() } else { super() } + /// ``` + fn enter_if_statement(&mut self, stmt: &mut IfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if !self.assignments.is_empty() { + let consequent_span = match &stmt.consequent { + Statement::ExpressionStatement(expr) + if expr.expression.is_super_call_expression() => + { + Some(expr.span) + } + _ => None, + }; + if let Some(span) = consequent_span { + let consequent = stmt.consequent.take_in(ctx.ast); + stmt.consequent = Self::create_block_with_statement(consequent, span, ctx); + } + + let alternate_span = match &stmt.alternate { + Some(Statement::ExpressionStatement(expr)) + if expr.expression.is_super_call_expression() => + { + Some(expr.span) + } + _ => None, + }; + if let Some(span) = alternate_span { + let alternate = stmt.alternate.take().unwrap(); + stmt.alternate = Some(Self::create_block_with_statement(alternate, span, ctx)); + } + } + + Self::replace_with_empty_block_if_ts(&mut stmt.consequent, ctx.current_scope_id(), ctx); + + if stmt.alternate.as_ref().is_some_and(Statement::is_typescript_syntax) { + stmt.alternate = None; + } + } + + fn enter_for_statement(&mut self, stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = stmt.scope_id(); + Self::replace_for_statement_body_with_empty_block_if_ts(&mut stmt.body, scope_id, ctx); + } + + fn enter_for_in_statement(&mut self, stmt: &mut ForInStatement<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = stmt.scope_id(); + Self::replace_for_statement_body_with_empty_block_if_ts(&mut stmt.body, scope_id, ctx); + } + + fn enter_for_of_statement(&mut self, stmt: &mut ForOfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + let scope_id = stmt.scope_id(); + Self::replace_for_statement_body_with_empty_block_if_ts(&mut stmt.body, scope_id, ctx); + } + + fn enter_while_statement(&mut self, stmt: &mut WhileStatement<'a>, ctx: &mut TraverseCtx<'a>) { + Self::replace_with_empty_block_if_ts(&mut stmt.body, ctx.current_scope_id(), ctx); + } + + fn enter_do_while_statement( + &mut self, + stmt: &mut DoWhileStatement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + Self::replace_with_empty_block_if_ts(&mut stmt.body, ctx.current_scope_id(), ctx); + } + + fn enter_tagged_template_expression( + &mut self, + expr: &mut TaggedTemplateExpression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + expr.type_arguments = None; + } + + fn enter_jsx_element(&mut self, _elem: &mut JSXElement<'a>, _ctx: &mut TraverseCtx<'a>) { + self.has_jsx_element = true; + } + + fn enter_jsx_fragment(&mut self, _elem: &mut JSXFragment<'a>, _ctx: &mut TraverseCtx<'a>) { + self.has_jsx_fragment = true; + } +} + +impl<'a> TypeScriptAnnotations<'a, '_> { + /// Check if the given name is a JSX pragma or fragment pragma import + /// and if the file contains JSX elements or fragments + fn is_jsx_imports(&self, name: &str) -> bool { + self.has_jsx_element && name == self.jsx_element_import_name + || self.has_jsx_fragment && name == self.jsx_fragment_import_name + } + + fn create_block_with_statement( + stmt: Statement<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let scope_id = ctx.insert_scope_below_statement(&stmt, ScopeFlags::empty()); + ctx.ast.statement_block_with_scope_id(span, ctx.ast.vec1(stmt), scope_id) + } + + fn replace_for_statement_body_with_empty_block_if_ts( + body: &mut Statement<'a>, + parent_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) { + Self::replace_with_empty_block_if_ts(body, parent_scope_id, ctx); + } + + fn replace_with_empty_block_if_ts( + stmt: &mut Statement<'a>, + parent_scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) { + if stmt.is_typescript_syntax() { + let scope_id = ctx.create_child_scope(parent_scope_id, ScopeFlags::empty()); + *stmt = ctx.ast.statement_block_with_scope_id(stmt.span(), ctx.ast.vec(), scope_id); + } + } + + fn has_value_reference(&self, id: &BindingIdentifier<'a>, ctx: &TraverseCtx<'a>) -> bool { + let symbol_id = id.symbol_id(); + + // `import T from 'mod'; const T = 1;` The T has a value redeclaration + // `import T from 'mod'; type T = number;` The T has a type redeclaration + // If the symbol is still a value symbol after `SymbolFlags::Import` is removed, then it's a value redeclaration. + // That means the import is shadowed, and we can safely remove the import. + if (ctx.scoping().symbol_flags(symbol_id) - SymbolFlags::Import).is_value() { + return false; + } + + if ctx.scoping().get_resolved_references(symbol_id).any(|reference| !reference.is_type()) { + return true; + } + + self.is_jsx_imports(&id.name) + } + + fn can_retain_export_specifier(specifier: &ExportSpecifier<'a>, ctx: &TraverseCtx<'a>) -> bool { + if specifier.export_kind.is_type() { + return false; + } + !matches!(&specifier.local, ModuleExportName::IdentifierReference(ident) if Self::is_refers_to_type(ident, ctx)) + } + + fn is_refers_to_type(ident: &IdentifierReference<'a>, ctx: &TraverseCtx<'a>) -> bool { + let scoping = ctx.scoping(); + let reference = scoping.get_reference(ident.reference_id()); + + reference.symbol_id().is_some_and(|symbol_id| { + reference.is_type() + || scoping.symbol_flags(symbol_id).is_ambient() + && scoping.symbol_redeclarations(symbol_id).iter().all(|r| r.flags.is_ambient()) + }) + } +} + +struct Assignment<'a> { + span: Span, + name: Atom<'a>, + symbol_id: SymbolId, +} + +impl<'a> Assignment<'a> { + // Creates `this.name = name` + fn create_this_property_assignment(&self, ctx: &mut TraverseCtx<'a>) -> Statement<'a> { + let reference_id = ctx.create_bound_reference(self.symbol_id, ReferenceFlags::Read); + let id = ctx.ast.identifier_reference_with_reference_id(self.span, self.name, reference_id); + + ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + SimpleAssignmentTarget::from(ctx.ast.member_expression_static( + SPAN, + ctx.ast.expression_this(SPAN), + ctx.ast.identifier_name(self.span, self.name), + false, + )) + .into(), + Expression::Identifier(ctx.alloc(id)), + ), + ) + } +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/class.rs b/crates/swc_ecma_transformer/oxc/typescript/class.rs new file mode 100644 index 000000000000..223862fa0fda --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/class.rs @@ -0,0 +1,422 @@ +use oxc_allocator::{TakeIn, Vec as ArenaVec}; +use oxc_ast::ast::*; +use oxc_semantic::ScopeFlags; +use oxc_span::SPAN; +use oxc_traverse::BoundIdentifier; + +use crate::{ + context::TraverseCtx, + utils::ast_builder::{ + create_class_constructor, create_this_property_access, create_this_property_assignment, + }, +}; + +use super::TypeScript; + +impl<'a> TypeScript<'a, '_> { + /// Transform class fields, and constructor parameters that includes modifiers into `this` assignments. + /// + /// This transformation is doing 2 things: + /// + /// 1. Convert constructor parameters that include modifier to `this` assignments and insert them + /// after the super call in the constructor body. + /// + /// Same as `Self::convert_constructor_params` does, the reason why we still need that method because + /// this method only calls when `set_public_class_fields` is `true` and `class_properties` plugin is + /// disabled, otherwise the `convert_constructor_params` method will be called. Merging them together + /// will increase unnecessary check when only transform constructor parameters. + /// + /// 2. Convert class fields to `this` assignments in the constructor body. + /// + /// > This transformation only works when `set_public_class_fields` is `true`, + /// > and the fields have initializers, which is to align with the behavior of TypeScript's + /// > `useDefineForClassFields: false` option. + /// + /// Input: + /// ```ts + /// class C { + /// x = 1; + /// [y] = 2; + /// } + /// ``` + /// + /// Output: + /// ```js + /// let _y; + /// class C { + /// static { + /// _y = y; + /// } + /// constructor() { + /// this.x = 1; + /// this[_y] = 2; + /// } + /// } + /// ``` + /// + /// The computed key transformation behavior is the same as `TypeScript`. + /// Computed key assignments are inserted into a static block, unlike Babel which inserts them before class. + /// We follow `TypeScript` just for simplicity, because `Babel` handles class expressions and class declarations + /// differently, which is quite troublesome to implement. + /// Anyway, `TypeScript` is the source of truth for the typescript transformation. + /// + /// For static properties, we convert them to static blocks. + /// + /// Input: + /// ```ts + /// class C { + /// static x = 1; + /// static [y] = 2; + /// } + /// ``` + /// + /// Output: + /// ```js + /// let _y; + /// class C { + /// static { + /// this.x = 1; + /// } + /// static { + /// this[_y] = 2; + /// } + /// } + /// ``` + /// + /// The transformation way is also the same as `TypeScript`, the advantage from the implementation is that + /// we don't need extra transformation for static properties, the output is the same as instance properties + /// transformation, and the greatest advantage is we don't need to care about `this` usage in static block. + pub(super) fn transform_class_fields(&self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + let mut constructor = None; + let mut property_assignments = Vec::new(); + let mut computed_key_assignments = Vec::new(); + for element in &mut class.body.body { + match element { + // `set_public_class_fields: true` only needs to transform non-private class fields. + ClassElement::PropertyDefinition(prop) if !prop.key.is_private_identifier() => { + if let Some(value) = prop.value.take() { + let assignment = self.convert_property_definition( + &mut prop.key, + value, + &mut computed_key_assignments, + ctx, + ); + if prop.r#static { + // Convert static property to static block + // `class C { static x = 1; }` -> `class C { static { this.x = 1; } }` + // `class C { static [x] = 1; }` -> `let _x; class C { static { this[_x] = 1; } }` + let body = ctx.ast.vec1(assignment); + *element = Self::create_class_static_block(body, ctx); + } else { + property_assignments.push(assignment); + } + } else if self.remove_class_fields_without_initializer + && let Some(key) = prop.key.as_expression_mut() + { + // `TypeScript` uses `isSimpleInlineableExpression` to check if the key needs to be kept. + // There is a little difference that we treat `BigIntLiteral` and `RegExpLiteral` can be kept, and + // `IdentifierReference` without symbol is not kept. + // https://github.com/microsoft/TypeScript/blob/8c62e08448e0ec76203bd519dd39608dbcb31705/src/compiler/transformers/classFields.ts#L2720 + if self.ctx.key_needs_temp_var(key, ctx) { + // When `remove_class_fields_without_initializer` is true, the property without initializer + // would be removed in the `transform_class_on_exit`. We need to make sure the computed key + // keeps and is evaluated in the same order as the original class field in static block. + computed_key_assignments.push(key.take_in(ctx.ast)); + } + } + } + ClassElement::MethodDefinition(method) => { + if method.kind == MethodDefinitionKind::Constructor { + constructor = Some(&mut method.value); + } else { + Self::convert_computed_key( + &mut method.key, + &mut computed_key_assignments, + ctx, + ); + } + } + ClassElement::AccessorProperty(accessor) => { + Self::convert_computed_key( + &mut accessor.key, + &mut computed_key_assignments, + ctx, + ); + } + _ => (), + } + } + + let computed_key_assignment_static_block = + (!computed_key_assignments.is_empty()).then(|| { + let sequence_expression = ctx + .ast + .expression_sequence(SPAN, ctx.ast.vec_from_iter(computed_key_assignments)); + let statement = ctx.ast.statement_expression(SPAN, sequence_expression); + Self::create_class_static_block(ctx.ast.vec1(statement), ctx) + }); + + if let Some(constructor) = constructor { + let params = &constructor.params.items; + + let params_assignment = Self::convert_constructor_params(params, ctx); + property_assignments.splice(0..0, params_assignment); + + // Exit if there are no property and parameter assignments + if property_assignments.is_empty() { + return; + } + + // `constructor {}` is guaranteed that it is `Some`. + let constructor_body_statements = &mut constructor.body.as_mut().unwrap().statements; + let super_call_position = Self::get_super_call_position(constructor_body_statements); + + // Insert the assignments after the `super()` call + constructor_body_statements + .splice(super_call_position..super_call_position, property_assignments); + + // Insert the static block after the constructor if there is a constructor + if let Some(element) = computed_key_assignment_static_block { + class.body.body.insert(0, element); + } + } else if !property_assignments.is_empty() { + // If there is no constructor, we need to create a default constructor + // that initializes the public fields + // TODO: should use `ctx.insert_scope_below_statements`, but it only accept an `ArenaVec` rather than std `Vec`. + let scope_id = ctx.create_child_scope_of_current( + ScopeFlags::StrictMode | ScopeFlags::Function | ScopeFlags::Constructor, + ); + let ctor = create_class_constructor( + property_assignments, + class.super_class.is_some(), + scope_id, + ctx, + ); + + // Insert the static block at the beginning of the class body if there is no constructor + if let Some(element) = computed_key_assignment_static_block { + class.body.body.splice(0..0, [ctor, element]); + } else { + // TODO(improve-on-babel): Could push constructor onto end of elements, instead of inserting as first + class.body.body.insert(0, ctor); + } + } else if let Some(element) = computed_key_assignment_static_block { + class.body.body.insert(0, element); + } + } + + pub(super) fn transform_class_on_exit( + &self, + class: &mut Class<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if !self.remove_class_fields_without_initializer { + return; + } + + class.body.body.retain(|element| { + if let ClassElement::PropertyDefinition(prop) = element + && prop.value.is_none() + && !prop.key.is_private_identifier() + { + return false; + } + true + }); + } + + /// Transform constructor parameters that include modifier to `this` assignments and + /// insert them after the super call in the constructor body. + /// + /// Input: + /// ```ts + /// class C { + /// constructor(public x, private y) {} + /// } + /// ``` + /// + /// Output: + /// ```js + /// class C { + /// constructor(x, y) { + /// this.x = x; + /// this.y = y; + /// } + /// ``` + pub(super) fn transform_class_constructor( + constructor: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !constructor.kind.is_constructor() || constructor.value.body.is_none() { + return; + } + + let params = &constructor.value.params.items; + let assignments = Self::convert_constructor_params(params, ctx).collect::>(); + + let constructor_body_statements = &mut constructor.value.body.as_mut().unwrap().statements; + let super_call_position = Self::get_super_call_position(constructor_body_statements); + + // Insert the assignments after the `super()` call + constructor_body_statements.splice(super_call_position..super_call_position, assignments); + } + + /// Convert property definition to `this` assignment in constructor. + /// + /// * Computed key: + /// `class C { [x()] = 1; }` -> `let _x; class C { static { _x = x(); } constructor() { this[_x] = 1; } }` + /// * Static key: + /// `class C { x = 1; }` -> `class C { constructor() { this.x = 1; } }` + /// + /// Returns an assignment statement which would be inserted in the constructor body. + fn convert_property_definition( + &self, + key: &mut PropertyKey<'a>, + value: Expression<'a>, + computed_key_assignments: &mut Vec>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let member = match key { + PropertyKey::StaticIdentifier(ident) => { + create_this_property_access(SPAN, ident.name, ctx) + } + PropertyKey::PrivateIdentifier(_) => { + unreachable!("PrivateIdentifier is skipped in transform_class_fields"); + } + key @ match_expression!(PropertyKey) => { + let key = key.to_expression_mut(); + // Note: Key can also be static `StringLiteral` or `NumericLiteral`. + // `class C { 'x' = true; 123 = false; }` + // No temp var is created for these. + let new_key = if self.ctx.key_needs_temp_var(key, ctx) { + let (assignment, ident) = + self.ctx.create_computed_key_temp_var(key.take_in(ctx.ast), ctx); + computed_key_assignments.push(assignment); + ident + } else { + key.take_in(ctx.ast) + }; + + ctx.ast.member_expression_computed( + SPAN, + ctx.ast.expression_this(SPAN), + new_key, + false, + ) + } + }; + let target = AssignmentTarget::from(member); + Self::create_assignment(target, value, ctx) + } + + /// Find the position of the `super()` call in the constructor body, otherwise return 0. + /// + /// Don't need to handle nested `super()` call because `TypeScript` doesn't allow it. + pub fn get_super_call_position(statements: &[Statement<'a>]) -> usize { + // Find the position of the `super()` call in the constructor body. + // Don't need to handle nested `super()` call because `TypeScript` doesn't allow it. + statements + .iter() + .position(|stmt| { + matches!(stmt, Statement::ExpressionStatement(stmt) + if stmt.expression.is_super_call_expression()) + }) + .map_or(0, |pos| pos + 1) + } + + /// Convert computed key to sequence expression if there are assignments. + /// + /// Input: + /// ```ts + /// class C { + /// [x()] = 1; + /// [y()]() {} + /// [x()] = 2; + /// } + /// ``` + /// + /// Output: + /// ```js + /// let _x, _x2; + /// class C { + /// constructor() { + /// this[_x] = 1; + /// this[_x2] = 2; + /// } + /// static { + /// _x2 = x(); + /// } + /// [(_x = x(), y())]() {} + /// } + /// ``` + /// + /// So that computed key keeps running in the same order as the original class field. + #[inline] + fn convert_computed_key( + key: &mut PropertyKey<'a>, + assignments: &mut Vec>, + ctx: &TraverseCtx<'a>, + ) { + if assignments.is_empty() { + return; + } + if let Some(key) = key.as_expression_mut() { + // If the key is already an expression, we need to create a new expression sequence + // to insert the assignments into. + let original_key = key.take_in(ctx.ast); + let new_key = ctx.ast.expression_sequence( + SPAN, + ctx.ast.vec_from_iter( + assignments.split_off(0).into_iter().chain(std::iter::once(original_key)), + ), + ); + *key = new_key; + } + } + + /// Convert constructor parameters that include modifier to `this` assignments + pub(super) fn convert_constructor_params( + params: &ArenaVec<'a, FormalParameter<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> impl Iterator> { + params + .iter() + .filter(|param| param.has_modifier()) + .filter_map(|param| param.pattern.get_binding_identifier()) + .map(|id| { + let target = create_this_property_assignment(id.span, id.name, ctx); + let value = BoundIdentifier::from_binding_ident(id).create_read_expression(ctx); + Self::create_assignment(target, value, ctx) + }) + } + + /// Create `a.b = value` + fn create_assignment( + target: AssignmentTarget<'a>, + value: Expression<'a>, + ctx: &TraverseCtx<'a>, + ) -> Statement<'a> { + ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, target, value), + ) + } + + /// Create `static { body }` + #[inline] + fn create_class_static_block( + body: ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> ClassElement<'a> { + let scope_id = ctx.insert_scope_below_statements( + &body, + ScopeFlags::StrictMode | ScopeFlags::ClassStaticBlock, + ); + + ctx.ast.class_element_static_block_with_scope_id( + SPAN, + ctx.ast.vec_from_iter(body), + scope_id, + ) + } +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/diagnostics.rs b/crates/swc_ecma_transformer/oxc/typescript/diagnostics.rs new file mode 100644 index 000000000000..200316a3be92 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/diagnostics.rs @@ -0,0 +1,34 @@ +use oxc_diagnostics::OxcDiagnostic; +use oxc_span::Span; + +pub fn import_equals_cannot_be_used_in_esm(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Import assignment cannot be used when targeting ECMAScript modules.") + .with_help( + "Consider using 'import * as ns from \"mod\"', + 'import {a} from \"mod\"', 'import d from \"mod\"', or another module format instead.", + ) + .with_label(span) + .with_error_code("TS", "1202") +} + +pub fn export_assignment_cannot_bed_used_in_esm(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Export assignment cannot be used when targeting ECMAScript modules.") + .with_help("Consider using 'export default' or another module format instead.") + .with_label(span) + .with_error_code("TS", "1203") +} + +pub fn ambient_module_nested(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Ambient modules cannot be nested in other modules or namespaces.") + .with_label(span) +} + +pub fn namespace_exporting_non_const(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Namespaces exporting non-const are not supported by Babel. Change to const or see: https://babeljs.io/docs/en/babel-plugin-transform-typescript") + .with_label(span) +} + +pub fn namespace_not_supported(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Namespace not marked type-only declare. Non-declarative namespaces are only supported experimentally in Babel. To enable and review caveats see: https://babeljs.io/docs/en/babel-plugin-transform-typescript") + .with_label(span) +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/enum.rs b/crates/swc_ecma_transformer/oxc/typescript/enum.rs new file mode 100644 index 000000000000..d63e3f75ef5e --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/enum.rs @@ -0,0 +1,650 @@ +use std::cell::Cell; + +use rustc_hash::FxHashMap; + +use oxc_allocator::{StringBuilder, TakeIn, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_ast_visit::{VisitMut, walk_mut}; +use oxc_data_structures::stack::NonEmptyStack; +use oxc_ecmascript::{ToInt32, ToUint32}; +use oxc_semantic::{ScopeFlags, ScopeId}; +use oxc_span::{Atom, SPAN, Span}; +use oxc_syntax::{ + number::{NumberBase, ToJsString}, + operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator}, + reference::ReferenceFlags, + symbol::SymbolFlags, +}; +use oxc_traverse::{BoundIdentifier, Traverse}; + +use crate::{context::TraverseCtx, state::TransformState}; + +/// enum member values (or None if it can't be evaluated at build time) keyed by names +type PrevMembers<'a> = FxHashMap, Option>>; + +pub struct TypeScriptEnum<'a> { + enums: FxHashMap, PrevMembers<'a>>, +} + +impl TypeScriptEnum<'_> { + pub fn new() -> Self { + Self { enums: FxHashMap::default() } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptEnum<'a> { + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let new_stmt = match stmt { + Statement::TSEnumDeclaration(ts_enum_decl) => { + self.transform_ts_enum(ts_enum_decl, None, ctx) + } + Statement::ExportNamedDeclaration(decl) => { + let span = decl.span; + if let Some(Declaration::TSEnumDeclaration(ts_enum_decl)) = &mut decl.declaration { + self.transform_ts_enum(ts_enum_decl, Some(span), ctx) + } else { + None + } + } + _ => None, + }; + + if let Some(new_stmt) = new_stmt { + *stmt = new_stmt; + } + } +} + +impl<'a> TypeScriptEnum<'a> { + /// ```TypeScript + /// enum Foo { + /// X = 1, + /// Y + /// } + /// ``` + /// ```JavaScript + /// var Foo = ((Foo) => { + /// Foo[Foo["X"] = 1] = "X"; + /// Foo[Foo["Y"] = 2] = "Y"; + /// return Foo; + /// })(Foo || {}); + /// ``` + fn transform_ts_enum( + &mut self, + decl: &mut TSEnumDeclaration<'a>, + export_span: Option, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if decl.declare { + return None; + } + + let ast = ctx.ast; + + let is_export = export_span.is_some(); + let is_not_top_scope = !ctx.scoping().scope_flags(ctx.current_scope_id()).is_top(); + + let enum_name = decl.id.name; + let func_scope_id = decl.scope_id(); + let param_binding = + ctx.generate_binding(enum_name, func_scope_id, SymbolFlags::FunctionScopedVariable); + + let id = param_binding.create_binding_pattern(ctx); + + // ((Foo) => { + let params = ast.formal_parameter(SPAN, ast.vec(), id, None, false, false); + let params = ast.vec1(params); + let params = ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::ArrowFormalParameters, + params, + NONE, + ); + + let has_potential_side_effect = decl.body.members.iter().any(|member| { + matches!( + member.initializer, + Some(Expression::NewExpression(_) | Expression::CallExpression(_)) + ) + }); + + let statements = self.transform_ts_enum_members( + decl.scope_id(), + &mut decl.body.members, + ¶m_binding, + ctx, + ); + let body = ast.alloc_function_body(decl.span, ast.vec(), statements); + let callee = ctx.ast.expression_function_with_scope_id_and_pure_and_pife( + SPAN, + FunctionType::FunctionExpression, + None, + false, + false, + false, + NONE, + NONE, + params, + NONE, + Some(body), + func_scope_id, + false, + false, + ); + + let enum_symbol_id = decl.id.symbol_id(); + + // Foo[Foo["X"] = 0] = "X"; + let redeclarations = ctx.scoping().symbol_redeclarations(enum_symbol_id); + let is_already_declared = + redeclarations.first().map_or_else(|| false, |rd| rd.span != decl.id.span); + + let arguments = if (is_export || is_not_top_scope) && !is_already_declared { + // }({}); + let object_expr = ast.expression_object(SPAN, ast.vec()); + ast.vec1(Argument::from(object_expr)) + } else { + // }(Foo || {}); + let op = LogicalOperator::Or; + let left = ctx.create_bound_ident_expr( + decl.id.span, + enum_name, + enum_symbol_id, + ReferenceFlags::Read, + ); + let right = ast.expression_object(SPAN, ast.vec()); + let expression = ast.expression_logical(SPAN, left, op, right); + ast.vec1(Argument::from(expression)) + }; + + let call_expression = ast.expression_call_with_pure( + SPAN, + callee, + NONE, + arguments, + false, + !has_potential_side_effect, + ); + + if is_already_declared { + let op = AssignmentOperator::Assign; + let left = ctx.create_bound_ident_reference( + decl.id.span, + enum_name, + enum_symbol_id, + ReferenceFlags::Write, + ); + let left = AssignmentTarget::AssignmentTargetIdentifier(ctx.alloc(left)); + let expr = ast.expression_assignment(SPAN, op, left, call_expression); + return Some(ast.statement_expression(decl.span, expr)); + } + + let kind = if is_export || is_not_top_scope { + VariableDeclarationKind::Let + } else { + VariableDeclarationKind::Var + }; + let decls = { + let binding_identifier = decl.id.clone(); + let binding_pattern_kind = + BindingPatternKind::BindingIdentifier(ctx.alloc(binding_identifier)); + let binding = ast.binding_pattern(binding_pattern_kind, NONE, false); + let decl = ast.variable_declarator(SPAN, kind, binding, Some(call_expression), false); + ast.vec1(decl) + }; + let variable_declaration = ast.declaration_variable(decl.span, kind, decls, false); + + let stmt = if let Some(export_span) = export_span { + let declaration = ctx + .ast + .plain_export_named_declaration_declaration(export_span, variable_declaration); + Statement::ExportNamedDeclaration(declaration) + } else { + Statement::from(variable_declaration) + }; + Some(stmt) + } + + fn transform_ts_enum_members( + &mut self, + enum_scope_id: ScopeId, + members: &mut ArenaVec<'a, TSEnumMember<'a>>, + param_binding: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ArenaVec<'a, Statement<'a>> { + let ast = ctx.ast; + + let mut statements = ast.vec(); + + // If enum number has no initializer, its value will be the previous member value + 1, + // if it's the first member, it will be `0`. + // It used to keep track of the previous constant number. + let mut prev_constant_number = Some(-1.0); + let mut previous_enum_members = self.enums.entry(param_binding.name).or_default().clone(); + + let mut prev_member_name = None; + + for member in members.take_in(ctx.ast) { + let member_name = member.id.static_name(); + + let init = if let Some(mut initializer) = member.initializer { + let constant_value = + self.computed_constant_value(&initializer, &previous_enum_members, ctx); + + previous_enum_members.insert(member_name, constant_value); + + match constant_value { + None => { + prev_constant_number = None; + + IdentifierReferenceRename::new( + param_binding.name, + enum_scope_id, + previous_enum_members.clone(), + ctx, + ) + .visit_expression(&mut initializer); + + initializer + } + Some(constant_value) => match constant_value { + ConstantValue::Number(v) => { + prev_constant_number = Some(v); + Self::get_initializer_expr(v, ctx) + } + ConstantValue::String(str) => { + prev_constant_number = None; + ast.expression_string_literal(SPAN, str, None) + } + }, + } + // No initializer, try to infer the value from the previous member. + } else if let Some(value) = &prev_constant_number { + let value = value + 1.0; + prev_constant_number = Some(value); + previous_enum_members.insert(member_name, Some(ConstantValue::Number(value))); + Self::get_initializer_expr(value, ctx) + } else if let Some(prev_member_name) = prev_member_name { + previous_enum_members.insert(member_name, None); + let self_ref = { + let obj = param_binding.create_read_expression(ctx); + let expr = ctx.ast.expression_string_literal(SPAN, prev_member_name, None); + ast.member_expression_computed(SPAN, obj, expr, false).into() + }; + + // 1 + Foo["x"] + let one = Self::get_number_literal_expression(1.0, ctx); + ast.expression_binary(SPAN, one, BinaryOperator::Addition, self_ref) + } else { + previous_enum_members.insert(member_name, Some(ConstantValue::Number(0.0))); + Self::get_number_literal_expression(0.0, ctx) + }; + + let is_str = init.is_string_literal(); + + // Foo["x"] = init + let member_expr = { + let obj = param_binding.create_read_expression(ctx); + let expr = ast.expression_string_literal(SPAN, member_name, None); + + ast.member_expression_computed(SPAN, obj, expr, false) + }; + let left = SimpleAssignmentTarget::from(member_expr); + let mut expr = + ast.expression_assignment(SPAN, AssignmentOperator::Assign, left.into(), init); + + // Foo[Foo["x"] = init] = "x" + if !is_str { + let member_expr = { + let obj = param_binding.create_read_expression(ctx); + ast.member_expression_computed(SPAN, obj, expr, false) + }; + let left = SimpleAssignmentTarget::from(member_expr); + let right = ast.expression_string_literal(SPAN, member_name, None); + expr = + ast.expression_assignment(SPAN, AssignmentOperator::Assign, left.into(), right); + } + + prev_member_name = Some(member_name); + statements.push(ast.statement_expression(member.span, expr)); + } + + self.enums.insert(param_binding.name, previous_enum_members.clone()); + + let enum_ref = param_binding.create_read_expression(ctx); + // return Foo; + let return_stmt = ast.statement_return(SPAN, Some(enum_ref)); + statements.push(return_stmt); + + statements + } + + fn get_number_literal_expression(value: f64, ctx: &TraverseCtx<'a>) -> Expression<'a> { + ctx.ast.expression_numeric_literal(SPAN, value, None, NumberBase::Decimal) + } + + fn get_initializer_expr(value: f64, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + let is_negative = value < 0.0; + + // Infinity + let expr = if value.is_infinite() { + let infinity_symbol_id = ctx.scoping().find_binding(ctx.current_scope_id(), "Infinity"); + ctx.create_ident_expr( + SPAN, + Atom::from("Infinity"), + infinity_symbol_id, + ReferenceFlags::Read, + ) + } else { + let value = if is_negative { -value } else { value }; + Self::get_number_literal_expression(value, ctx) + }; + + if is_negative { + ctx.ast.expression_unary(SPAN, UnaryOperator::UnaryNegation, expr) + } else { + expr + } + } +} + +#[derive(Debug, Clone, Copy)] +enum ConstantValue<'a> { + Number(f64), + String(Atom<'a>), +} + +impl<'a> TypeScriptEnum<'a> { + /// Evaluate the expression to a constant value. + /// Refer to [babel](https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L241C1-L394C2) + fn computed_constant_value( + &self, + expr: &Expression<'a>, + prev_members: &PrevMembers<'a>, + ctx: &TraverseCtx<'a>, + ) -> Option> { + self.evaluate(expr, prev_members, ctx) + } + + fn evaluate_ref( + &self, + expr: &Expression<'a>, + prev_members: &PrevMembers<'a>, + ) -> Option> { + match expr { + match_member_expression!(Expression) => { + let expr = expr.to_member_expression(); + let Expression::Identifier(ident) = expr.object() else { return None }; + let members = self.enums.get(&ident.name)?; + let property = expr.static_property_name()?; + *members.get(property)? + } + Expression::Identifier(ident) => { + if ident.name == "Infinity" { + return Some(ConstantValue::Number(f64::INFINITY)); + } else if ident.name == "NaN" { + return Some(ConstantValue::Number(f64::NAN)); + } + + if let Some(value) = prev_members.get(&ident.name) { + return *value; + } + + // TODO: + // This is a bit tricky because we need to find the BindingIdentifier that corresponds to the identifier reference. + // and then we may to evaluate the initializer of the BindingIdentifier. + // finally, we can get the value of the identifier and call the `computed_constant_value` function. + // See https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L327-L329 + None + } + _ => None, + } + } + + fn evaluate( + &self, + expr: &Expression<'a>, + prev_members: &PrevMembers<'a>, + ctx: &TraverseCtx<'a>, + ) -> Option> { + match expr { + Expression::Identifier(_) + | Expression::ComputedMemberExpression(_) + | Expression::StaticMemberExpression(_) + | Expression::PrivateFieldExpression(_) => self.evaluate_ref(expr, prev_members), + Expression::BinaryExpression(expr) => { + self.eval_binary_expression(expr, prev_members, ctx) + } + Expression::UnaryExpression(expr) => { + self.eval_unary_expression(expr, prev_members, ctx) + } + Expression::NumericLiteral(lit) => Some(ConstantValue::Number(lit.value)), + Expression::StringLiteral(lit) => Some(ConstantValue::String(lit.value)), + Expression::TemplateLiteral(lit) => { + let value = if let Some(quasi) = lit.single_quasi() { + quasi + } else { + let mut value = StringBuilder::new_in(ctx.ast.allocator); + for (i, quasi) in lit.quasis.iter().enumerate() { + value.push_str(&quasi.value.cooked.unwrap_or(quasi.value.raw)); + if i < lit.expressions.len() { + match self.evaluate(&lit.expressions[i], prev_members, ctx)? { + ConstantValue::String(str) => value.push_str(&str), + ConstantValue::Number(num) => value.push_str(&num.to_js_string()), + } + } + } + Atom::from(value.into_str()) + }; + Some(ConstantValue::String(value)) + } + Expression::ParenthesizedExpression(expr) => { + self.evaluate(&expr.expression, prev_members, ctx) + } + _ => None, + } + } + + fn eval_binary_expression( + &self, + expr: &BinaryExpression<'a>, + prev_members: &PrevMembers<'a>, + ctx: &TraverseCtx<'a>, + ) -> Option> { + let left = self.evaluate(&expr.left, prev_members, ctx)?; + let right = self.evaluate(&expr.right, prev_members, ctx)?; + + if matches!(expr.operator, BinaryOperator::Addition) + && (matches!(left, ConstantValue::String(_)) + || matches!(right, ConstantValue::String(_))) + { + let left_string = match left { + ConstantValue::String(str) => str, + ConstantValue::Number(v) => ctx.ast.atom(&v.to_js_string()), + }; + + let right_string = match right { + ConstantValue::String(str) => str, + ConstantValue::Number(v) => ctx.ast.atom(&v.to_js_string()), + }; + + return Some(ConstantValue::String( + ctx.ast.atom_from_strs_array([&left_string, &right_string]), + )); + } + + let left = match left { + ConstantValue::Number(v) => v, + ConstantValue::String(_) => return None, + }; + + let right = match right { + ConstantValue::Number(v) => v, + ConstantValue::String(_) => return None, + }; + + match expr.operator { + BinaryOperator::ShiftRight => Some(ConstantValue::Number(f64::from( + left.to_int_32().wrapping_shr(right.to_uint_32()), + ))), + BinaryOperator::ShiftRightZeroFill => Some(ConstantValue::Number(f64::from( + (left.to_uint_32()).wrapping_shr(right.to_uint_32()), + ))), + BinaryOperator::ShiftLeft => Some(ConstantValue::Number(f64::from( + left.to_int_32().wrapping_shl(right.to_uint_32()), + ))), + BinaryOperator::BitwiseXOR => { + Some(ConstantValue::Number(f64::from(left.to_int_32() ^ right.to_int_32()))) + } + BinaryOperator::BitwiseOR => { + Some(ConstantValue::Number(f64::from(left.to_int_32() | right.to_int_32()))) + } + BinaryOperator::BitwiseAnd => { + Some(ConstantValue::Number(f64::from(left.to_int_32() & right.to_int_32()))) + } + BinaryOperator::Multiplication => Some(ConstantValue::Number(left * right)), + BinaryOperator::Division => Some(ConstantValue::Number(left / right)), + BinaryOperator::Addition => Some(ConstantValue::Number(left + right)), + BinaryOperator::Subtraction => Some(ConstantValue::Number(left - right)), + BinaryOperator::Remainder => Some(ConstantValue::Number(left % right)), + BinaryOperator::Exponential => Some(ConstantValue::Number(left.powf(right))), + _ => None, + } + } + + fn eval_unary_expression( + &self, + expr: &UnaryExpression<'a>, + prev_members: &PrevMembers<'a>, + ctx: &TraverseCtx<'a>, + ) -> Option> { + let value = self.evaluate(&expr.argument, prev_members, ctx)?; + + let value = match value { + ConstantValue::Number(value) => value, + ConstantValue::String(_) => { + let value = if expr.operator == UnaryOperator::UnaryNegation { + ConstantValue::Number(f64::NAN) + } else if expr.operator == UnaryOperator::BitwiseNot { + ConstantValue::Number(-1.0) + } else { + value + }; + return Some(value); + } + }; + + match expr.operator { + UnaryOperator::UnaryPlus => Some(ConstantValue::Number(value)), + UnaryOperator::UnaryNegation => Some(ConstantValue::Number(-value)), + UnaryOperator::BitwiseNot => Some(ConstantValue::Number(f64::from(!value.to_int_32()))), + _ => None, + } + } +} + +/// Rename the identifier references in the enum members to `enum_name.identifier` +/// ```ts +/// enum A { +/// a = 1, +/// b = a.toString(), +/// d = c, +/// } +/// ``` +/// will be transformed to +/// ```ts +/// enum A { +/// a = 1, +/// b = A.a.toString(), +/// d = A.c, +/// } +/// ``` +struct IdentifierReferenceRename<'a, 'ctx> { + enum_name: Atom<'a>, + previous_enum_members: PrevMembers<'a>, + scope_stack: NonEmptyStack, + ctx: &'ctx TraverseCtx<'a>, +} + +impl<'a, 'ctx> IdentifierReferenceRename<'a, 'ctx> { + fn new( + enum_name: Atom<'a>, + enum_scope_id: ScopeId, + previous_enum_members: PrevMembers<'a>, + ctx: &'ctx TraverseCtx<'a>, + ) -> Self { + IdentifierReferenceRename { + enum_name, + previous_enum_members, + scope_stack: NonEmptyStack::new(enum_scope_id), + ctx, + } + } +} + +impl IdentifierReferenceRename<'_, '_> { + fn should_reference_enum_member(&self, ident: &IdentifierReference<'_>) -> bool { + // Don't need to rename the identifier if it's not a member of the enum, + if !self.previous_enum_members.contains_key(&ident.name) { + return false; + } + + let scoping = self.ctx.scoping.scoping(); + let Some(symbol_id) = scoping.get_reference(ident.reference_id()).symbol_id() else { + // No symbol found, yet the name is found in previous_enum_members. + // It must be referencing a member declared in a previous enum block: `enum Foo { A }; enum Foo { B = A }` + return true; + }; + + let symbol_scope_id = scoping.symbol_scope_id(symbol_id); + // Don't need to rename the identifier when it references a nested enum member: + // + // ```ts + // enum OuterEnum { + // A = 0, + // B = () => { + // enum InnerEnum { + // A = 0, + // B = A, + // ^ This references to `InnerEnum.A` should not be renamed + // } + // return InnerEnum.B; + // } + // } + // ``` + *self.scope_stack.first() == symbol_scope_id + // The resolved symbol is declared outside the enum, + // and we have checked that the name exists in previous_enum_members: + // + // ```ts + // const A = 0; + // enum Foo { A } + // enum Foo { B = A } + // ^ This should be renamed to Foo.A + // ``` + || !self.scope_stack.contains(&symbol_scope_id) + } +} + +impl<'a> VisitMut<'a> for IdentifierReferenceRename<'a, '_> { + fn enter_scope(&mut self, _flags: ScopeFlags, scope_id: &Cell>) { + self.scope_stack.push(scope_id.get().unwrap()); + } + + fn leave_scope(&mut self) { + self.scope_stack.pop(); + } + + fn visit_expression(&mut self, expr: &mut Expression<'a>) { + match expr { + Expression::Identifier(ident) if self.should_reference_enum_member(ident) => { + let object = self.ctx.ast.expression_identifier(SPAN, self.enum_name); + let property = self.ctx.ast.identifier_name(SPAN, ident.name); + *expr = self.ctx.ast.member_expression_static(SPAN, object, property, false).into(); + } + _ => { + walk_mut::walk_expression(self, expr); + } + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/mod.rs b/crates/swc_ecma_transformer/oxc/typescript/mod.rs new file mode 100644 index 000000000000..a43f8c588e4c --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/mod.rs @@ -0,0 +1,318 @@ +use oxc_allocator::Vec as ArenaVec; +use oxc_ast::ast::*; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +mod annotations; +mod class; +mod diagnostics; +mod r#enum; +mod module; +mod namespace; +mod options; +mod rewrite_extensions; + +use annotations::TypeScriptAnnotations; +use r#enum::TypeScriptEnum; +use module::TypeScriptModule; +use namespace::TypeScriptNamespace; +pub use options::{RewriteExtensionsMode, TypeScriptOptions}; +use rewrite_extensions::TypeScriptRewriteExtensions; + +/// [Preset TypeScript](https://babeljs.io/docs/babel-preset-typescript) +/// +/// This preset includes the following plugins: +/// +/// * [transform-typescript](https://babeljs.io/docs/babel-plugin-transform-typescript) +/// +/// This plugin adds support for the types syntax used by the TypeScript programming language. +/// However, this plugin does not add the ability to type-check the JavaScript passed to it. +/// For that, you will need to install and set up TypeScript. +/// +/// Note that although the TypeScript compiler tsc actively supports certain JavaScript proposals such as optional chaining (?.), +/// nullish coalescing (??) and class properties (this.#x), this preset does not include these features +/// because they are not the types syntax available in TypeScript only. +/// We recommend using preset-env with preset-typescript if you want to transpile these features. +/// +/// This plugin is included in `preset-typescript`. +/// +/// ## Example +/// +/// In: `const x: number = 0;` +/// Out: `const x = 0;` +pub struct TypeScript<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + + annotations: TypeScriptAnnotations<'a, 'ctx>, + r#enum: TypeScriptEnum<'a>, + namespace: TypeScriptNamespace<'a, 'ctx>, + module: TypeScriptModule<'a, 'ctx>, + rewrite_extensions: Option, + // Options + remove_class_fields_without_initializer: bool, +} + +impl<'a, 'ctx> TypeScript<'a, 'ctx> { + pub fn new(options: &TypeScriptOptions, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + ctx, + annotations: TypeScriptAnnotations::new(options, ctx), + r#enum: TypeScriptEnum::new(), + namespace: TypeScriptNamespace::new(options, ctx), + module: TypeScriptModule::new(options.only_remove_type_imports, ctx), + rewrite_extensions: TypeScriptRewriteExtensions::new(options), + remove_class_fields_without_initializer: !options.allow_declare_fields + || options.remove_class_fields_without_initializer, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for TypeScript<'a, '_> { + fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.ctx.source_type.is_typescript_definition() { + // Output empty file for TS definitions + program.directives.clear(); + program.hashbang = None; + program.body.clear(); + } else { + program.source_type = program.source_type.with_javascript(true); + self.namespace.enter_program(program, ctx); + } + } + + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.exit_program(program, ctx); + self.module.exit_program(program, ctx); + ctx.scoping.delete_typescript_bindings(); + } + + fn enter_arrow_function_expression( + &mut self, + expr: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_arrow_function_expression(expr, ctx); + } + + fn enter_variable_declarator( + &mut self, + decl: &mut VariableDeclarator<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_variable_declarator(decl, ctx); + } + + fn enter_binding_pattern(&mut self, pat: &mut BindingPattern<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_binding_pattern(pat, ctx); + } + + fn enter_call_expression(&mut self, expr: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_call_expression(expr, ctx); + } + + fn enter_chain_element(&mut self, element: &mut ChainElement<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_chain_element(element, ctx); + } + + fn enter_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_class(class, ctx); + + // Avoid converting class fields when class-properties plugin is enabled, that plugin has covered all + // this transformation does. + if !self.ctx.is_class_properties_plugin_enabled + && self.ctx.assumptions.set_public_class_fields + { + self.transform_class_fields(class, ctx); + } + } + + fn exit_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.exit_class(class, ctx); + + // Avoid converting class fields when class-properties plugin is enabled, that plugin has covered all + // this transformation does. + if !self.ctx.is_class_properties_plugin_enabled { + self.transform_class_on_exit(class, ctx); + } + } + + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_expression(expr, ctx); + } + + fn enter_simple_assignment_target( + &mut self, + target: &mut SimpleAssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_simple_assignment_target(target, ctx); + } + + fn enter_assignment_target( + &mut self, + target: &mut AssignmentTarget<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_assignment_target(target, ctx); + } + + fn enter_formal_parameter( + &mut self, + param: &mut FormalParameter<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_formal_parameter(param, ctx); + } + + fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.exit_function(func, ctx); + } + + fn enter_jsx_opening_element( + &mut self, + elem: &mut JSXOpeningElement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_jsx_opening_element(elem, ctx); + } + + fn enter_method_definition( + &mut self, + def: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_method_definition(def, ctx); + if self.ctx.is_class_properties_plugin_enabled + || !self.ctx.assumptions.set_public_class_fields + { + Self::transform_class_constructor(def, ctx); + } + } + + fn enter_new_expression(&mut self, expr: &mut NewExpression<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_new_expression(expr, ctx); + } + + fn enter_property_definition( + &mut self, + def: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_property_definition(def, ctx); + } + + fn enter_accessor_property( + &mut self, + def: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_accessor_property(def, ctx); + } + + fn enter_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_statements(stmts, ctx); + } + + fn exit_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.exit_statements(stmts, ctx); + } + + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + self.r#enum.enter_statement(stmt, ctx); + self.module.enter_statement(stmt, ctx); + } + + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.exit_statement(stmt, ctx); + } + + fn enter_if_statement(&mut self, stmt: &mut IfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_if_statement(stmt, ctx); + } + + fn enter_while_statement(&mut self, stmt: &mut WhileStatement<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_while_statement(stmt, ctx); + } + + fn enter_do_while_statement( + &mut self, + stmt: &mut DoWhileStatement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_do_while_statement(stmt, ctx); + } + + fn enter_for_statement(&mut self, stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_for_statement(stmt, ctx); + } + + fn enter_for_in_statement(&mut self, stmt: &mut ForInStatement<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_for_in_statement(stmt, ctx); + } + + fn enter_for_of_statement(&mut self, stmt: &mut ForOfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_for_of_statement(stmt, ctx); + } + + fn enter_tagged_template_expression( + &mut self, + expr: &mut TaggedTemplateExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.annotations.enter_tagged_template_expression(expr, ctx); + } + + fn enter_jsx_element(&mut self, elem: &mut JSXElement<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_jsx_element(elem, ctx); + } + + fn enter_jsx_fragment(&mut self, elem: &mut JSXFragment<'a>, ctx: &mut TraverseCtx<'a>) { + self.annotations.enter_jsx_fragment(elem, ctx); + } + + fn enter_declaration(&mut self, node: &mut Declaration<'a>, ctx: &mut TraverseCtx<'a>) { + self.module.enter_declaration(node, ctx); + } + + fn enter_import_declaration( + &mut self, + node: &mut ImportDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(rewrite_extensions) = &mut self.rewrite_extensions { + rewrite_extensions.enter_import_declaration(node, ctx); + } + } + + fn enter_export_all_declaration( + &mut self, + node: &mut ExportAllDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(rewrite_extensions) = &mut self.rewrite_extensions { + rewrite_extensions.enter_export_all_declaration(node, ctx); + } + } + + fn enter_export_named_declaration( + &mut self, + node: &mut ExportNamedDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(rewrite_extensions) = &mut self.rewrite_extensions { + rewrite_extensions.enter_export_named_declaration(node, ctx); + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/module.rs b/crates/swc_ecma_transformer/oxc/typescript/module.rs new file mode 100644 index 000000000000..636ce8910e3c --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/module.rs @@ -0,0 +1,199 @@ +use oxc_allocator::TakeIn; +use oxc_ast::{NONE, ast::*}; +use oxc_semantic::{Reference, SymbolFlags}; +use oxc_span::SPAN; +use oxc_syntax::reference::ReferenceFlags; +use oxc_traverse::Traverse; + +use super::diagnostics; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct TypeScriptModule<'a, 'ctx> { + /// + only_remove_type_imports: bool, + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> TypeScriptModule<'a, 'ctx> { + pub fn new(only_remove_type_imports: bool, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { only_remove_type_imports, ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptModule<'a, '_> { + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + // In Babel, it will insert `use strict` in `@babel/transform-modules-commonjs` plugin. + // Once we have a commonjs plugin, we can consider moving this logic there. + if self.ctx.module.is_commonjs() { + let has_use_strict = program.directives.iter().any(Directive::is_use_strict); + if !has_use_strict { + program.directives.insert(0, ctx.ast.use_strict_directive()); + } + } + } + + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Statement::TSExportAssignment(export_assignment) = stmt { + *stmt = self.transform_ts_export_assignment(export_assignment, ctx); + } + } + + fn enter_declaration(&mut self, decl: &mut Declaration<'a>, ctx: &mut TraverseCtx<'a>) { + if let Declaration::TSImportEqualsDeclaration(import_equals) = decl + && import_equals.import_kind.is_value() + && let Some(new_decl) = self.transform_ts_import_equals(import_equals, ctx) + { + *decl = new_decl; + } + } +} + +impl<'a> TypeScriptModule<'a, '_> { + /// Transform `export = expression` to `module.exports = expression`. + fn transform_ts_export_assignment( + &self, + export_assignment: &mut TSExportAssignment<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + if self.ctx.module.is_esm() { + self.ctx.error(diagnostics::export_assignment_cannot_bed_used_in_esm( + export_assignment.span, + )); + } + + // module.exports + let module_exports = { + let reference_id = + ctx.create_reference_in_current_scope("module", ReferenceFlags::Read); + let reference = + ctx.ast.alloc_identifier_reference_with_reference_id(SPAN, "module", reference_id); + let object = Expression::Identifier(reference); + let property = ctx.ast.identifier_name(SPAN, "exports"); + ctx.ast.member_expression_static(SPAN, object, property, false) + }; + + let left = AssignmentTarget::from(SimpleAssignmentTarget::from(module_exports)); + let right = export_assignment.expression.take_in(ctx.ast); + let assignment_expr = + ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, left, right); + ctx.ast.statement_expression(SPAN, assignment_expr) + } + + /// Transform TSImportEqualsDeclaration to a VariableDeclaration. + /// + /// ```TypeScript + /// import module = require('module'); + /// import AliasModule = LongNameModule; + /// + /// ```JavaScript + /// const module = require('module'); + /// const AliasModule = LongNameModule; + /// ``` + fn transform_ts_import_equals( + &self, + decl: &mut TSImportEqualsDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if !self.only_remove_type_imports + && !ctx.parent().is_export_named_declaration() + && ctx.scoping().get_resolved_references(decl.id.symbol_id()).all(Reference::is_type) + { + // No value reference, we will remove this declaration in `TypeScriptAnnotations` + match &mut decl.module_reference { + module_reference @ match_ts_type_name!(TSModuleReference) => { + if let Some(ident) = + module_reference.to_ts_type_name().get_identifier_reference() + { + let reference = ctx.scoping_mut().get_reference_mut(ident.reference_id()); + // The binding of TSImportEqualsDeclaration has treated as a type reference, + // so an identifier reference that it referenced also should be treated as a type reference. + // `import TypeBinding = X.Y.Z` + // ^ `X` should be treated as a type reference. + let flags = reference.flags_mut(); + debug_assert_eq!(*flags, ReferenceFlags::Read); + *flags = ReferenceFlags::Type; + } + } + TSModuleReference::ExternalModuleReference(_) => {} + } + let scope_id = ctx.current_scope_id(); + ctx.scoping_mut().remove_binding(scope_id, &decl.id.name); + return None; + } + + let binding_pattern_kind = + BindingPatternKind::BindingIdentifier(ctx.ast.alloc(decl.id.clone())); + let binding = ctx.ast.binding_pattern(binding_pattern_kind, NONE, false); + let decl_span = decl.span; + + let flags = ctx.scoping_mut().symbol_flags_mut(decl.id.symbol_id()); + flags.remove(SymbolFlags::Import); + + let (kind, init) = match &mut decl.module_reference { + type_name @ match_ts_type_name!(TSModuleReference) => { + flags.insert(SymbolFlags::FunctionScopedVariable); + + ( + VariableDeclarationKind::Var, + self.transform_ts_type_name(&mut *type_name.to_ts_type_name_mut(), ctx), + ) + } + TSModuleReference::ExternalModuleReference(reference) => { + flags.insert(SymbolFlags::BlockScopedVariable | SymbolFlags::ConstVariable); + + if self.ctx.module.is_esm() { + self.ctx.error(diagnostics::import_equals_cannot_be_used_in_esm(decl_span)); + } + + let require_symbol_id = + ctx.scoping().find_binding(ctx.current_scope_id(), "require"); + let callee = ctx.create_ident_expr( + SPAN, + Atom::from("require"), + require_symbol_id, + ReferenceFlags::Read, + ); + let arguments = + ctx.ast.vec1(Argument::StringLiteral(ctx.alloc(reference.expression.clone()))); + ( + VariableDeclarationKind::Const, + ctx.ast.expression_call(SPAN, callee, NONE, arguments, false), + ) + } + }; + let decls = + ctx.ast.vec1(ctx.ast.variable_declarator(SPAN, kind, binding, Some(init), false)); + + Some(ctx.ast.declaration_variable(SPAN, kind, decls, false)) + } + + #[expect(clippy::only_used_in_recursion)] + fn transform_ts_type_name( + &self, + type_name: &mut TSTypeName<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match type_name { + TSTypeName::IdentifierReference(ident) => { + let ident = ident.clone(); + let reference = ctx.scoping_mut().get_reference_mut(ident.reference_id()); + *reference.flags_mut() = ReferenceFlags::Read; + Expression::Identifier(ctx.alloc(ident)) + } + TSTypeName::QualifiedName(qualified_name) => ctx + .ast + .member_expression_static( + SPAN, + self.transform_ts_type_name(&mut qualified_name.left, ctx), + qualified_name.right.clone(), + false, + ) + .into(), + TSTypeName::ThisExpression(e) => ctx.ast.expression_this(e.span), + } + } +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/namespace.rs b/crates/swc_ecma_transformer/oxc/typescript/namespace.rs new file mode 100644 index 000000000000..72a9595a75bb --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/namespace.rs @@ -0,0 +1,497 @@ +use oxc_allocator::{Box as ArenaBox, TakeIn, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_ecmascript::BoundNames; +use oxc_span::SPAN; +use oxc_syntax::{ + operator::{AssignmentOperator, LogicalOperator}, + scope::{ScopeFlags, ScopeId}, + symbol::SymbolFlags, +}; +use oxc_traverse::{BoundIdentifier, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +use super::{ + TypeScriptOptions, + diagnostics::{ambient_module_nested, namespace_exporting_non_const, namespace_not_supported}, +}; + +pub struct TypeScriptNamespace<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, + + // Options + allow_namespaces: bool, +} + +impl<'a, 'ctx> TypeScriptNamespace<'a, 'ctx> { + pub fn new(options: &TypeScriptOptions, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { ctx, allow_namespaces: options.allow_namespaces } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptNamespace<'a, '_> { + // `namespace Foo { }` -> `let Foo; (function (_Foo) { })(Foo || (Foo = {}));` + fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + // namespace declaration is only allowed at the top level + if !has_namespace(program.body.as_slice()) { + return; + } + + // Recreate the statements vec for memory efficiency. + // Inserting the `let` declaration multiple times will reallocate the whole statements vec + // every time a namespace declaration is encountered. + let mut new_stmts = ctx.ast.vec(); + + for stmt in program.body.take_in(ctx.ast) { + match stmt { + Statement::TSModuleDeclaration(decl) => { + if !self.allow_namespaces { + self.ctx.error(namespace_not_supported(decl.span)); + } + + self.handle_nested(decl, /* is_export */ false, &mut new_stmts, None, ctx); + continue; + } + Statement::ExportNamedDeclaration(export_decl) + if export_decl.declaration.as_ref().is_some_and(|declaration| { + !declaration.declare() + && matches!(declaration, Declaration::TSModuleDeclaration(_)) + }) => + { + let Some(Declaration::TSModuleDeclaration(decl)) = + export_decl.unbox().declaration + else { + unreachable!() + }; + + if !self.allow_namespaces { + self.ctx.error(namespace_not_supported(decl.span)); + } + + self.handle_nested(decl, /* is_export */ true, &mut new_stmts, None, ctx); + continue; + } + _ => {} + } + + new_stmts.push(stmt); + } + + program.body = new_stmts; + } +} + +impl<'a> TypeScriptNamespace<'a, '_> { + fn handle_nested( + &self, + decl: ArenaBox<'a, TSModuleDeclaration<'a>>, + is_export: bool, + parent_stmts: &mut ArenaVec<'a, Statement<'a>>, + parent_binding: Option<&BoundIdentifier<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + if decl.declare { + return; + } + + // Skip empty declaration e.g. `namespace x;` + let TSModuleDeclaration { span, id, body, scope_id, .. } = decl.unbox(); + + let TSModuleDeclarationName::Identifier(ident) = id else { + self.ctx.error(ambient_module_nested(span)); + return; + }; + + // Check if this is an empty namespace or only contains type declarations + let symbol_id = ident.symbol_id(); + let flags = ctx.scoping().symbol_flags(symbol_id); + + // If it's a namespace, we need additional checks to determine if it can return early. + if flags.is_namespace_module() { + // Don't need further check because NO `ValueModule` namespace redeclaration + if !flags.is_value_module() { + return; + } + + // Input: + // ```ts + // // SymbolFlags: NameSpaceModule + // export namespace Foo { + // export type T = 0; + // } + // // SymbolFlags: ValueModule + // export namespace Foo { + // export const Bar = 1; + // } + // ``` + // + // Output: + // ```js + // // SymbolFlags: ValueModule + // export let Foo; + // (function(_Foo) { + // const Bar = _Foo.Bar = 1; + // })(Foo || (Foo = {})); + // ``` + // + // When both `NameSpaceModule` and `ValueModule` are present, we need to check the current + // declaration flags. If the current declaration is `NameSpaceModule`, we can return early + // because it's a type-only namespace and doesn't emit any JS code, otherwise we need to + // continue transforming it. + + // Find the current declaration flag + let current_declaration_flags = ctx + .scoping() + .symbol_redeclarations(symbol_id) + .iter() + .find(|rd| rd.span == ident.span) + .unwrap() + .flags; + + // Return if the current declaration is a namespace + if current_declaration_flags.is_namespace_module() { + return; + } + } + + let Some(body) = body else { + return; + }; + + let binding = BoundIdentifier::from_binding_ident(&ident); + + // Reuse `TSModuleDeclaration`'s scope in transformed function + let scope_id = scope_id.get().unwrap(); + let uid_binding = + ctx.generate_uid(&binding.name, scope_id, SymbolFlags::FunctionScopedVariable); + + let directives; + let namespace_top_level; + + match body { + TSModuleDeclarationBody::TSModuleBlock(block) => { + let block = block.unbox(); + directives = block.directives; + namespace_top_level = block.body; + } + // We handle `namespace X.Y {}` as if it was + // namespace X { + // export namespace Y {} + // } + TSModuleDeclarationBody::TSModuleDeclaration(declaration) => { + let declaration = Declaration::TSModuleDeclaration(declaration); + let export_named_decl = + ctx.ast.plain_export_named_declaration_declaration(SPAN, declaration); + let stmt = Statement::ExportNamedDeclaration(export_named_decl); + directives = ctx.ast.vec(); + namespace_top_level = ctx.ast.vec1(stmt); + } + } + + let mut new_stmts = ctx.ast.vec(); + + for stmt in namespace_top_level { + match stmt { + Statement::TSModuleDeclaration(decl) => { + self.handle_nested(decl, /* is_export */ false, &mut new_stmts, None, ctx); + } + Statement::ExportNamedDeclaration(export_decl) => { + // NB: `ExportNamedDeclaration` with no declaration (e.g. `export {x}`) is not + // legal syntax in TS namespaces + let export_decl = export_decl.unbox(); + if let Some(decl) = export_decl.declaration { + if decl.declare() { + continue; + } + match decl { + Declaration::TSImportEqualsDeclaration(ref import_equals) => { + let binding = + BoundIdentifier::from_binding_ident(&import_equals.id); + new_stmts.push(Statement::from(decl)); + Self::add_declaration(&uid_binding, &binding, &mut new_stmts, ctx); + } + Declaration::TSEnumDeclaration(ref enum_decl) => { + let binding = BoundIdentifier::from_binding_ident(&enum_decl.id); + new_stmts.push(Statement::from(decl)); + Self::add_declaration(&uid_binding, &binding, &mut new_stmts, ctx); + } + Declaration::ClassDeclaration(ref class_decl) => { + // Class declaration always has a binding + let binding = BoundIdentifier::from_binding_ident( + class_decl.id.as_ref().unwrap(), + ); + new_stmts.push(Statement::from(decl)); + Self::add_declaration(&uid_binding, &binding, &mut new_stmts, ctx); + } + Declaration::FunctionDeclaration(ref func_decl) + if !func_decl.is_typescript_syntax() => + { + // Function declaration always has a binding + let binding = BoundIdentifier::from_binding_ident( + func_decl.id.as_ref().unwrap(), + ); + new_stmts.push(Statement::from(decl)); + Self::add_declaration(&uid_binding, &binding, &mut new_stmts, ctx); + } + Declaration::VariableDeclaration(var_decl) => { + var_decl.declarations.iter().for_each(|decl| { + if !decl.kind.is_const() { + self.ctx.error(namespace_exporting_non_const(decl.span)); + } + }); + let stmts = + Self::handle_variable_declaration(var_decl, &uid_binding, ctx); + new_stmts.extend(stmts); + } + Declaration::TSModuleDeclaration(module_decl) => { + self.handle_nested( + module_decl, + /* is_export */ + false, + &mut new_stmts, + Some(&uid_binding), + ctx, + ); + } + _ => {} + } + } + } + _ => new_stmts.push(stmt), + } + } + + if !Self::is_redeclaration_namespace(&ident, ctx) { + let declaration = Self::create_variable_declaration(&binding, ctx); + if is_export { + let export_named_decl = + ctx.ast.plain_export_named_declaration_declaration(SPAN, declaration); + let stmt = Statement::ExportNamedDeclaration(export_named_decl); + parent_stmts.push(stmt); + } else { + parent_stmts.push(Statement::from(declaration)); + } + } + let func_body = ctx.ast.function_body(SPAN, directives, new_stmts); + + parent_stmts.push(Self::transform_namespace( + span, + &uid_binding, + &binding, + parent_binding, + func_body, + scope_id, + ctx, + )); + } + + // `namespace Foo { }` -> `let Foo; (function (_Foo) { })(Foo || (Foo = {}));` + // ^^^^^^^ + fn create_variable_declaration( + binding: &BoundIdentifier<'a>, + ctx: &TraverseCtx<'a>, + ) -> Declaration<'a> { + let kind = VariableDeclarationKind::Let; + let declarations = { + let pattern = binding.create_binding_pattern(ctx); + let decl = ctx.ast.variable_declarator(SPAN, kind, pattern, None, false); + ctx.ast.vec1(decl) + }; + ctx.ast.declaration_variable(SPAN, kind, declarations, false) + } + + // `namespace Foo { }` -> `let Foo; (function (_Foo) { })(Foo || (Foo = {}));` + fn transform_namespace( + span: Span, + param_binding: &BoundIdentifier<'a>, + binding: &BoundIdentifier<'a>, + parent_binding: Option<&BoundIdentifier<'a>>, + func_body: FunctionBody<'a>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + // `(function (_N) { var x; })(N || (N = {}))`; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^ + let callee = { + let params = { + let pattern = param_binding.create_binding_pattern(ctx); + let items = ctx.ast.vec1(ctx.ast.plain_formal_parameter(SPAN, pattern)); + ctx.ast.formal_parameters(SPAN, FormalParameterKind::FormalParameter, items, NONE) + }; + let function_expr = + Expression::FunctionExpression(ctx.ast.alloc_plain_function_with_scope_id( + FunctionType::FunctionExpression, + SPAN, + None, + params, + func_body, + scope_id, + )); + *ctx.scoping_mut().scope_flags_mut(scope_id) = + ScopeFlags::Function | ScopeFlags::StrictMode; + ctx.ast.expression_parenthesized(SPAN, function_expr) + }; + + // (function (_N) { var M; (function (_M) { var x; })(M || (M = _N.M || (_N.M = {})));})(N || (N = {})); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ + // Nested namespace arguments Normal namespace arguments + let arguments = { + // M + let logical_left = binding.create_read_expression(ctx); + + // (_N.M = {}) or (N = {}) + let mut logical_right = { + // _N.M + let assign_left = if let Some(parent_binding) = parent_binding { + AssignmentTarget::from(ctx.ast.member_expression_static( + SPAN, + parent_binding.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, binding.name), + false, + )) + } else { + // _N + binding.create_write_target(ctx) + }; + + let assign_right = ctx.ast.expression_object(SPAN, ctx.ast.vec()); + let op = AssignmentOperator::Assign; + let assign_expr = + ctx.ast.expression_assignment(SPAN, op, assign_left, assign_right); + ctx.ast.expression_parenthesized(SPAN, assign_expr) + }; + + // (M = _N.M || (_N.M = {})) + if let Some(parent_binding) = parent_binding { + let assign_left = binding.create_write_target(ctx); + let assign_right = { + let property = ctx.ast.identifier_name(SPAN, binding.name); + let logical_left = ctx.ast.member_expression_static( + SPAN, + parent_binding.create_read_expression(ctx), + property, + false, + ); + let op = LogicalOperator::Or; + ctx.ast.expression_logical(SPAN, logical_left.into(), op, logical_right) + }; + let op = AssignmentOperator::Assign; + logical_right = ctx.ast.expression_assignment(SPAN, op, assign_left, assign_right); + logical_right = ctx.ast.expression_parenthesized(SPAN, logical_right); + } + + let expr = + ctx.ast.expression_logical(SPAN, logical_left, LogicalOperator::Or, logical_right); + ctx.ast.vec1(Argument::from(expr)) + }; + + let expr = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false); + ctx.ast.statement_expression(span, expr) + } + + /// Add assignment statement for decl id + /// function id() {} -> function id() {}; Name.id = id; + fn add_declaration( + namespace_binding: &BoundIdentifier<'a>, + value_binding: &BoundIdentifier<'a>, + new_stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + let assignment_statement = + Self::create_assignment_statement(namespace_binding, value_binding, ctx); + let assignment_statement = ctx.ast.statement_expression(SPAN, assignment_statement); + new_stmts.push(assignment_statement); + } + + // parent_binding.binding = binding + fn create_assignment_statement( + object_binding: &BoundIdentifier<'a>, + value_binding: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let object = object_binding.create_read_expression(ctx); + let property = ctx.ast.identifier_name(SPAN, value_binding.name); + let left = ctx.ast.member_expression_static(SPAN, object, property, false); + let left = AssignmentTarget::from(left); + let right = value_binding.create_read_expression(ctx); + let op = AssignmentOperator::Assign; + ctx.ast.expression_assignment(SPAN, op, left, right) + } + + /// Convert `export const foo = 1` to `Namespace.foo = 1`; + fn handle_variable_declaration( + mut var_decl: ArenaBox<'a, VariableDeclaration<'a>>, + binding: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> ArenaVec<'a, Statement<'a>> { + let is_all_binding_identifier = var_decl + .declarations + .iter() + .all(|declaration| declaration.id.kind.is_binding_identifier()); + + // `export const a = 1` transforms to `const a = N.a = 1`, the output + // is smaller than `const a = 1; N.a = a`; + if is_all_binding_identifier { + var_decl.declarations.iter_mut().for_each(|declarator| { + let Some(property_name) = declarator.id.get_identifier_name() else { + return; + }; + if let Some(init) = &mut declarator.init { + declarator.init = Some( + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + SimpleAssignmentTarget::from(ctx.ast.member_expression_static( + SPAN, + binding.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, property_name), + false, + )) + .into(), + init.take_in(ctx.ast), + ), + ); + } + }); + return ctx.ast.vec1(Statement::VariableDeclaration(var_decl)); + } + + // Now we have pattern in declarators + // `export const [a] = 1` transforms to `const [a] = 1; N.a = a` + let mut assignments = ctx.ast.vec(); + var_decl.bound_names(&mut |id| { + assignments.push(Self::create_assignment_statement( + binding, + &BoundIdentifier::from_binding_ident(id), + ctx, + )); + }); + + ctx.ast.vec_from_array([ + Statement::VariableDeclaration(var_decl), + ctx.ast.statement_expression(SPAN, ctx.ast.expression_sequence(SPAN, assignments)), + ]) + } + + /// Check the namespace binding identifier if it is a redeclaration + fn is_redeclaration_namespace(id: &BindingIdentifier<'a>, ctx: &TraverseCtx<'a>) -> bool { + let symbol_id = id.symbol_id(); + let redeclarations = ctx.scoping().symbol_redeclarations(symbol_id); + // Find first value declaration because only value declaration will emit JS code. + redeclarations.iter().find(|rd| rd.flags.is_value()).is_some_and(|rd| rd.span != id.span) + } +} + +/// Check if the statements contain a namespace declaration +fn has_namespace(stmts: &[Statement]) -> bool { + stmts.iter().any(|stmt| match stmt { + Statement::TSModuleDeclaration(_) => true, + Statement::ExportNamedDeclaration(decl) => { + matches!(decl.declaration, Some(Declaration::TSModuleDeclaration(_))) + } + _ => false, + }) +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/options.rs b/crates/swc_ecma_transformer/oxc/typescript/options.rs new file mode 100644 index 000000000000..059dcd6a7b8a --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/options.rs @@ -0,0 +1,168 @@ +use std::{borrow::Cow, fmt}; + +use serde::{ + Deserialize, Deserializer, + de::{self, Visitor}, +}; + +fn default_for_jsx_pragma() -> Cow<'static, str> { + Cow::Borrowed("React.createElement") +} + +fn default_for_jsx_pragma_frag() -> Cow<'static, str> { + Cow::Borrowed("React.Fragment") +} + +fn default_as_true() -> bool { + true +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct TypeScriptOptions { + /// Replace the function used when compiling JSX expressions. + /// This is so that we know that the import is not a type import, and should not be removed. + /// defaults to React + #[serde(default = "default_for_jsx_pragma")] + pub jsx_pragma: Cow<'static, str>, + + /// Replace the function used when compiling JSX fragment expressions. + /// This is so that we know that the import is not a type import, and should not be removed. + /// defaults to React.Fragment + #[serde(default = "default_for_jsx_pragma_frag")] + pub jsx_pragma_frag: Cow<'static, str>, + + /// When set to true, the transform will only remove type-only imports (introduced in TypeScript 3.8). + /// This should only be used if you are using TypeScript >= 3.8. + pub only_remove_type_imports: bool, + + // Enables compilation of TypeScript namespaces. + #[serde(default = "default_as_true")] + pub allow_namespaces: bool, + + /// When enabled, type-only class fields are only removed if they are prefixed with the declare modifier: + /// + /// ## Deprecated + /// + /// Allowing `declare` fields is built-in support in Oxc without any option. If you want to remove class fields + /// without initializer, you can use `remove_class_fields_without_initializer: true` instead. + #[serde(default = "default_as_true")] + pub allow_declare_fields: bool, + + /// When enabled, class fields without initializers are removed. + /// + /// For example: + /// ```ts + /// class Foo { + /// x: number; + /// y: number = 0; + /// } + /// ``` + /// // transform into + /// ```js + /// class Foo { + /// x: number; + /// } + /// ``` + /// + /// The option is used to align with the behavior of TypeScript's `useDefineForClassFields: false` option. + /// When you want to enable this, you also need to set [`crate::CompilerAssumptions::set_public_class_fields`] + /// to `true`. The `set_public_class_fields: true` + `remove_class_fields_without_initializer: true` is + /// equivalent to `useDefineForClassFields: false` in TypeScript. + /// + /// When `set_public_class_fields` is true and class-properties plugin is enabled, the above example transforms into: + /// + /// ```js + /// class Foo { + /// constructor() { + /// this.y = 0; + /// } + /// } + /// ``` + /// + /// Defaults to `false`. + #[serde(default)] + pub remove_class_fields_without_initializer: bool, + + /// Unused. + pub optimize_const_enums: bool, + + // Preset options + /// Modifies extensions in import and export declarations. + /// + /// This option, when used together with TypeScript's [`allowImportingTsExtension`](https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions) option, + /// allows writing complete relative specifiers in import declarations while using the same extension used by the source files. + /// + /// When set to `true`, same as [`RewriteExtensionsMode::Rewrite`]. Defaults to `false` (do nothing). + #[serde(deserialize_with = "deserialize_rewrite_import_extensions")] + pub rewrite_import_extensions: Option, +} + +impl Default for TypeScriptOptions { + fn default() -> Self { + Self { + jsx_pragma: default_for_jsx_pragma(), + jsx_pragma_frag: default_for_jsx_pragma_frag(), + only_remove_type_imports: false, + allow_namespaces: default_as_true(), + allow_declare_fields: default_as_true(), + remove_class_fields_without_initializer: false, + optimize_const_enums: false, + rewrite_import_extensions: None, + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub enum RewriteExtensionsMode { + /// Rewrite `.ts`/`.mts`/`.cts` extensions in import/export declarations to `.js`/`.mjs`/`.cjs`. + #[default] + Rewrite, + /// Remove `.ts`/`.mts`/`.cts`/`.tsx` extensions in import/export declarations. + Remove, +} + +impl RewriteExtensionsMode { + pub fn is_remove(self) -> bool { + matches!(self, Self::Remove) + } +} + +pub fn deserialize_rewrite_import_extensions<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct RewriteExtensionsModeVisitor; + + impl Visitor<'_> for RewriteExtensionsModeVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("true, false, \"rewrite\", or \"remove\"") + } + + fn visit_bool(self, value: bool) -> Result + where + E: de::Error, + { + if value { Ok(Some(RewriteExtensionsMode::Rewrite)) } else { Ok(None) } + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "rewrite" => Ok(Some(RewriteExtensionsMode::Rewrite)), + "remove" => Ok(Some(RewriteExtensionsMode::Remove)), + _ => Err(E::custom(format!( + "Expected RewriteExtensionsMode is either \"rewrite\" or \"remove\" but found: {value}" + ))), + } + } + } + + deserializer.deserialize_any(RewriteExtensionsModeVisitor) +} diff --git a/crates/swc_ecma_transformer/oxc/typescript/rewrite_extensions.rs b/crates/swc_ecma_transformer/oxc/typescript/rewrite_extensions.rs new file mode 100644 index 000000000000..6544ad8fa176 --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/typescript/rewrite_extensions.rs @@ -0,0 +1,86 @@ +//! Rewrite import extensions +//! +//! This plugin is used to rewrite/remove extensions from import/export source. +//! It is only handled source that contains `/` or `\` in the source. +//! +//! Based on Babel's [plugin-rewrite-ts-imports](https://github.com/babel/babel/blob/3bcfee232506a4cebe410f02042fb0f0adeeb0b1/packages/babel-preset-typescript/src/plugin-rewrite-ts-imports.ts) + +use oxc_ast::ast::{ + ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration, StringLiteral, +}; +use oxc_span::Atom; +use oxc_traverse::Traverse; + +use crate::{TypeScriptOptions, context::TraverseCtx, state::TransformState}; + +use super::options::RewriteExtensionsMode; + +pub struct TypeScriptRewriteExtensions { + mode: RewriteExtensionsMode, +} + +impl TypeScriptRewriteExtensions { + pub fn new(options: &TypeScriptOptions) -> Option { + options.rewrite_import_extensions.map(|mode| Self { mode }) + } + + pub fn rewrite_extensions<'a>(&self, source: &mut StringLiteral<'a>, ctx: &TraverseCtx<'a>) { + let value = source.value.as_str(); + if !value.contains(['/', '\\']) { + return; + } + + let Some((without_extension, extension)) = value.rsplit_once('.') else { return }; + + let replace = match extension { + "mts" => ".mjs", + "cts" => ".cjs", + "ts" | "tsx" => ".js", + _ => return, // do not rewrite or remove other unknown extensions + }; + + source.value = if self.mode.is_remove() { + Atom::from(without_extension) + } else { + ctx.ast.atom_from_strs_array([without_extension, replace]) + }; + source.raw = None; + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptRewriteExtensions { + fn enter_import_declaration( + &mut self, + node: &mut ImportDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if node.import_kind.is_type() { + return; + } + self.rewrite_extensions(&mut node.source, ctx); + } + + fn enter_export_named_declaration( + &mut self, + node: &mut ExportNamedDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if node.export_kind.is_type() { + return; + } + if let Some(source) = node.source.as_mut() { + self.rewrite_extensions(source, ctx); + } + } + + fn enter_export_all_declaration( + &mut self, + node: &mut ExportAllDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if node.export_kind.is_type() { + return; + } + self.rewrite_extensions(&mut node.source, ctx); + } +} diff --git a/crates/swc_ecma_transformer/oxc/utils/ast_builder.rs b/crates/swc_ecma_transformer/oxc/utils/ast_builder.rs new file mode 100644 index 000000000000..e7c2ee589f2c --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/utils/ast_builder.rs @@ -0,0 +1,222 @@ +use std::iter; + +use oxc_allocator::{Box as ArenaBox, Vec as ArenaVec}; +use oxc_ast::{NONE, ast::*}; +use oxc_semantic::{ReferenceFlags, ScopeFlags, ScopeId, SymbolFlags}; +use oxc_span::{GetSpan, SPAN}; +use oxc_traverse::BoundIdentifier; + +use crate::context::TraverseCtx; + +/// `object` -> `object.call`. +pub fn create_member_callee<'a>( + object: Expression<'a>, + property: &'static str, + ctx: &TraverseCtx<'a>, +) -> Expression<'a> { + let property = ctx.ast.identifier_name(SPAN, Atom::from(property)); + Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)) +} + +/// `object` -> `object.bind(this)`. +pub fn create_bind_call<'a>( + callee: Expression<'a>, + this: Expression<'a>, + span: Span, + ctx: &TraverseCtx<'a>, +) -> Expression<'a> { + let callee = create_member_callee(callee, "bind", ctx); + let arguments = ctx.ast.vec1(Argument::from(this)); + ctx.ast.expression_call(span, callee, NONE, arguments, false) +} + +/// `object` -> `object.call(...arguments)`. +pub fn create_call_call<'a>( + callee: Expression<'a>, + this: Expression<'a>, + span: Span, + ctx: &TraverseCtx<'a>, +) -> Expression<'a> { + let callee = create_member_callee(callee, "call", ctx); + let arguments = ctx.ast.vec1(Argument::from(this)); + ctx.ast.expression_call(span, callee, NONE, arguments, false) +} + +/// Wrap an `Expression` in an arrow function IIFE (immediately invoked function expression) +/// with a body block. +/// +/// `expr` -> `(() => { return expr; })()` +pub fn wrap_expression_in_arrow_function_iife<'a>( + expr: Expression<'a>, + ctx: &mut TraverseCtx<'a>, +) -> Expression<'a> { + let scope_id = + ctx.insert_scope_below_expression(&expr, ScopeFlags::Arrow | ScopeFlags::Function); + let span = expr.span(); + let stmts = ctx.ast.vec1(ctx.ast.statement_return(SPAN, Some(expr))); + wrap_statements_in_arrow_function_iife(stmts, scope_id, span, ctx) +} + +/// Wrap statements in an IIFE (immediately invoked function expression). +/// +/// `x; y; z;` -> `(() => { x; y; z; })()` +pub fn wrap_statements_in_arrow_function_iife<'a>( + stmts: ArenaVec<'a, Statement<'a>>, + scope_id: ScopeId, + span: Span, + ctx: &TraverseCtx<'a>, +) -> Expression<'a> { + let kind = FormalParameterKind::ArrowFormalParameters; + let params = ctx.ast.alloc_formal_parameters(SPAN, kind, ctx.ast.vec(), NONE); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), stmts); + let arrow = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife( + SPAN, false, false, NONE, params, NONE, body, scope_id, false, false, + ); + ctx.ast.expression_call(span, arrow, NONE, ctx.ast.vec(), false) +} + +/// `object` -> `object.prototype`. +pub fn create_prototype_member<'a>( + object: Expression<'a>, + ctx: &TraverseCtx<'a>, +) -> Expression<'a> { + let property = ctx.ast.identifier_name(SPAN, Atom::from("prototype")); + let static_member = ctx.ast.member_expression_static(SPAN, object, property, false); + Expression::from(static_member) +} + +/// `object` -> `object.a`. +pub fn create_property_access<'a>( + span: Span, + object: Expression<'a>, + property: &str, + ctx: &TraverseCtx<'a>, +) -> Expression<'a> { + let property = ctx.ast.identifier_name(SPAN, ctx.ast.atom(property)); + Expression::from(ctx.ast.member_expression_static(span, object, property, false)) +} + +/// `this.property` +#[inline] +pub fn create_this_property_access<'a>( + span: Span, + property: Atom<'a>, + ctx: &TraverseCtx<'a>, +) -> MemberExpression<'a> { + let object = ctx.ast.expression_this(span); + let property = ctx.ast.identifier_name(SPAN, property); + ctx.ast.member_expression_static(span, object, property, false) +} + +/// `this.property` +#[inline] +pub fn create_this_property_assignment<'a>( + span: Span, + property: Atom<'a>, + ctx: &TraverseCtx<'a>, +) -> AssignmentTarget<'a> { + AssignmentTarget::from(create_this_property_access(span, property, ctx)) +} + +/// Create assignment to a binding. +pub fn create_assignment<'a>( + binding: &BoundIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, +) -> Expression<'a> { + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + binding.create_target(ReferenceFlags::Write, ctx), + value, + ) +} + +/// `super(...args);` +pub fn create_super_call<'a>( + args_binding: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, +) -> Expression<'a> { + ctx.ast.expression_call( + SPAN, + ctx.ast.expression_super(SPAN), + NONE, + ctx.ast + .vec1(ctx.ast.argument_spread_element(SPAN, args_binding.create_read_expression(ctx))), + false, + ) +} + +/// * With super class: +/// `constructor(..._args) { super(..._args); statements }` +/// * Without super class: +// `constructor() { statements }` +pub fn create_class_constructor<'a, 'c>( + stmts_iter: impl IntoIterator> + 'c, + has_super_class: bool, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, +) -> ClassElement<'a> { + // Add `super(..._args);` statement and `..._args` param if class has a super class. + // `constructor(..._args) { super(..._args); /* prop initialization */ }` + // TODO(improve-on-babel): We can use `arguments` instead of creating `_args`. + let mut params_rest = None; + let stmts = if has_super_class { + let args_binding = ctx.generate_uid("args", scope_id, SymbolFlags::FunctionScopedVariable); + params_rest = Some( + ctx.ast.alloc_binding_rest_element(SPAN, args_binding.create_binding_pattern(ctx)), + ); + ctx.ast.vec_from_iter( + iter::once(ctx.ast.statement_expression(SPAN, create_super_call(&args_binding, ctx))) + .chain(stmts_iter), + ) + } else { + ctx.ast.vec_from_iter(stmts_iter) + }; + + let params = ctx.ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + ctx.ast.vec(), + params_rest, + ); + + create_class_constructor_with_params(stmts, params, scope_id, ctx) +} + +// `constructor(params) { statements }` +pub fn create_class_constructor_with_params<'a>( + stmts: ArenaVec<'a, Statement<'a>>, + params: ArenaBox<'a, FormalParameters<'a>>, + scope_id: ScopeId, + ctx: &TraverseCtx<'a>, +) -> ClassElement<'a> { + ClassElement::MethodDefinition(ctx.ast.alloc_method_definition( + SPAN, + MethodDefinitionType::MethodDefinition, + ctx.ast.vec(), + PropertyKey::StaticIdentifier( + ctx.ast.alloc_identifier_name(SPAN, Atom::from("constructor")), + ), + ctx.ast.alloc_function_with_scope_id( + SPAN, + FunctionType::FunctionExpression, + None, + false, + false, + false, + NONE, + NONE, + params, + NONE, + Some(ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), stmts)), + scope_id, + ), + MethodDefinitionKind::Constructor, + false, + false, + false, + false, + None, + )) +} diff --git a/crates/swc_ecma_transformer/oxc/utils/mod.rs b/crates/swc_ecma_transformer/oxc/utils/mod.rs new file mode 100644 index 000000000000..dca0fb0f720e --- /dev/null +++ b/crates/swc_ecma_transformer/oxc/utils/mod.rs @@ -0,0 +1 @@ +pub mod ast_builder; diff --git a/crates/swc_ecma_transformer/src/lib.rs b/crates/swc_ecma_transformer/src/lib.rs index 430561979cf6..cf453e892c66 100644 --- a/crates/swc_ecma_transformer/src/lib.rs +++ b/crates/swc_ecma_transformer/src/lib.rs @@ -61,3 +61,4 @@ impl Options { hook_pass(hook) } } +