diff --git a/crates/oxc_semantic/src/reference.rs b/crates/oxc_semantic/src/reference.rs index 4bf1ecc4e63e1..544e17d8887fd 100644 --- a/crates/oxc_semantic/src/reference.rs +++ b/crates/oxc_semantic/src/reference.rs @@ -81,7 +81,7 @@ impl Reference { } #[inline] - pub(crate) fn set_symbol_id(&mut self, symbol_id: SymbolId) { + pub fn set_symbol_id(&mut self, symbol_id: SymbolId) { self.symbol_id = Some(symbol_id); } diff --git a/crates/oxc_semantic/src/scope.rs b/crates/oxc_semantic/src/scope.rs index df526f5bfaed1..612e6c93150fc 100644 --- a/crates/oxc_semantic/src/scope.rs +++ b/crates/oxc_semantic/src/scope.rs @@ -351,6 +351,13 @@ impl ScopeTree { } } + /// Rename a binding to a new name. + pub fn rename_binding(&mut self, scope_id: ScopeId, old_name: &str, new_name: CompactStr) { + if let Some(symbol_id) = self.bindings[scope_id].shift_remove(old_name) { + self.bindings[scope_id].insert(new_name, symbol_id); + } + } + /// Reserve memory for an `additional` number of scopes. pub fn reserve(&mut self, additional: usize) { self.parent_ids.reserve(additional); diff --git a/crates/oxc_semantic/src/symbol.rs b/crates/oxc_semantic/src/symbol.rs index 3c220cbc6e5a0..1527997e53ccf 100644 --- a/crates/oxc_semantic/src/symbol.rs +++ b/crates/oxc_semantic/src/symbol.rs @@ -102,6 +102,12 @@ impl SymbolTable { &self.names[symbol_id] } + /// Rename a symbol. + #[inline] + pub fn rename(&mut self, symbol_id: SymbolId, new_name: CompactStr) { + self.names[symbol_id] = new_name; + } + #[inline] pub fn set_name(&mut self, symbol_id: SymbolId, name: CompactStr) { self.names[symbol_id] = name; diff --git a/crates/oxc_transformer/src/common/arrow_function_converter.rs b/crates/oxc_transformer/src/common/arrow_function_converter.rs index 9de158b3e68af..acde13c691ff3 100644 --- a/crates/oxc_transformer/src/common/arrow_function_converter.rs +++ b/crates/oxc_transformer/src/common/arrow_function_converter.rs @@ -87,16 +87,18 @@ //! The Implementation based on //! +use rustc_hash::{FxHashMap, FxHashSet}; + use oxc_allocator::{Box as ArenaBox, String as ArenaString, Vec as ArenaVec}; use oxc_ast::{ast::*, AstBuilder, NONE}; use oxc_data_structures::stack::SparseStack; -use oxc_span::SPAN; +use oxc_semantic::{ReferenceFlags, SymbolId}; +use oxc_span::{CompactStr, SPAN}; use oxc_syntax::{ scope::{ScopeFlags, ScopeId}, symbol::SymbolFlags, }; use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx}; -use rustc_hash::FxHashMap; use crate::EnvOptions; @@ -125,6 +127,8 @@ struct SuperMethodInfo<'a> { pub struct ArrowFunctionConverter<'a> { mode: ArrowFunctionConverterMode, this_var_stack: SparseStack>, + arguments_var_stack: SparseStack>, + renamed_arguments_symbol_ids: FxHashSet, super_methods: Option, SuperMethodInfo<'a>>>, } @@ -137,8 +141,14 @@ impl<'a> ArrowFunctionConverter<'a> { } else { ArrowFunctionConverterMode::Disabled }; - // `SparseStack` is created with 1 empty entry, for `Program` - Self { mode, this_var_stack: SparseStack::new(), super_methods: None } + // `SparseStack`s are created with 1 empty entry, for `Program` + Self { + mode, + this_var_stack: SparseStack::new(), + arguments_var_stack: SparseStack::new(), + renamed_arguments_symbol_ids: FxHashSet::default(), + super_methods: None, + } } } @@ -153,14 +163,19 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { } 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, ctx, ); + debug_assert!(self.this_var_stack.len() == 1); debug_assert!(self.this_var_stack.last().is_none()); + debug_assert!(self.arguments_var_stack.len() == 1); + debug_assert!(self.arguments_var_stack.last().is_none()); } fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { @@ -169,6 +184,7 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { } self.this_var_stack.push(None); + self.arguments_var_stack.push(None); if self.is_async_only() && func.r#async && Self::is_class_method_like_ancestor(ctx.parent()) { self.super_methods = Some(FxHashMap::default()); @@ -196,10 +212,12 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { return; }; let this_var = self.this_var_stack.pop(); + let arguments_var = self.arguments_var_stack.pop(); self.insert_variable_statement_at_the_top_of_statements( scope_id, &mut body.statements, this_var, + arguments_var, ctx, ); } @@ -222,6 +240,8 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { block.scope_id(), &mut block.body, this_var, + // `arguments` is not allowed to be used in static blocks + None, ctx, ); } @@ -301,6 +321,22 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { *expr = Self::transform_arrow_function_expression(arrow_function_expr, ctx); } } + + fn enter_identifier_reference( + &mut self, + ident: &mut IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.transform_identifier_reference_for_arguments(ident, ctx); + } + + fn enter_binding_identifier( + &mut self, + ident: &mut BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.transform_binding_identifier_for_arguments(ident, ctx); + } } impl<'a> ArrowFunctionConverter<'a> { @@ -787,28 +823,196 @@ impl<'a> ArrowFunctionConverter<'a> { ast.atom(name.into_bump_str()) } + /// Whether to transform the `arguments` identifier. + fn should_transform_arguments_identifier(&self, name: &str, ctx: &mut TraverseCtx<'a>) -> bool { + self.is_async_only() && name == "arguments" && Self::is_affected_arguments_identifier(ctx) + } + + /// Check if the `arguments` identifier is affected by the transformation. + fn is_affected_arguments_identifier(ctx: &mut TraverseCtx<'a>) -> bool { + let mut ancestors = ctx.ancestors().skip(1); + while let Some(ancestor) = ancestors.next() { + match ancestor { + Ancestor::ArrowFunctionExpressionParams(arrow) => { + if *arrow.r#async() { + return true; + } + } + Ancestor::ArrowFunctionExpressionBody(arrow) => { + if *arrow.r#async() { + return true; + } + } + Ancestor::FunctionBody(func) => { + return *func.r#async() + && Self::is_class_method_like_ancestor(ancestors.next().unwrap()); + } + _ => (), + } + } + + false + } + + /// Rename the `arguments` symbol to a new name. + fn rename_arguments_symbol(symbol_id: SymbolId, name: CompactStr, ctx: &mut TraverseCtx<'a>) { + let scope_id = ctx.symbols().get_scope_id(symbol_id); + ctx.symbols_mut().rename(symbol_id, name.clone()); + ctx.scopes_mut().rename_binding(scope_id, "arguments", name); + } + + /// 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 !self.should_transform_arguments_identifier(&ident.name, ctx) { + return; + } + + let reference_id = ident.reference_id(); + let symbol_id = ctx.symbols().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"); + let arguments_name_atom = ctx.ast.atom(&arguments_name); + 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_atom, 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.scopes().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.symbols_mut().get_reference_mut(reference_id); + reference.set_symbol_id(binding.symbol_id); + ctx.scopes_mut().delete_root_unresolved_reference(&ident.name, reference_id); + ctx.symbols_mut().resolved_references[binding.symbol_id].push(reference_id); + } + + ident.name = binding.name.clone(); + } + + /// 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>, + ) { + if ctx.current_scope_flags().is_strict_mode() // `arguments` is not allowed to be defined in strict mode. + || !self.should_transform_arguments_identifier(&ident.name, ctx) + { + return; + } + + self.arguments_var_stack.last_or_init(|| { + let arguments_name = ctx.generate_uid_name("arguments"); + ident.name = ctx.ast.atom(&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.clone(), 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 reference = + ctx.create_unbound_ident_reference(SPAN, Atom::from("arguments"), ReferenceFlags::Read); + let mut init = Expression::Identifier(ctx.ast.alloc(reference.clone())); + + // Top level may doesn't have `arguments`, so we need to check it. + // `typeof arguments === "undefined" ? void 0 : arguments;` + if ctx.scopes().root_scope_id() == target_scope_id { + let argument = Expression::Identifier(ctx.ast.alloc(reference)); + let typeof_arguments = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, argument); + let undefined_literal = ctx.ast.expression_string_literal(SPAN, "undefined"); + 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( &mut self, target_scope_id: ScopeId, statements: &mut ArenaVec<'a, Statement<'a>>, this_var: Option>, + arguments_var: Option>, ctx: &mut TraverseCtx<'a>, ) { + // `_arguments = arguments;` + let arguments = self.create_arguments_var_declarator(target_scope_id, arguments_var, ctx); + + let is_class_method_like = Self::is_class_method_like_ancestor(ctx.parent()); + let declarations_count = usize::from(arguments.is_some()) + + if is_class_method_like { + self.super_methods.as_ref().map_or(0, FxHashMap::len) + } else { + 0 + } + + 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.getSomething;` - let mut declarations = if Self::is_class_method_like_ancestor(ctx.parent()) { + if is_class_method_like { if let Some(super_methods) = self.super_methods.as_mut() { - let mut declarations = ctx.ast.vec_with_capacity(super_methods.len() + 1); declarations.extend(super_methods.drain().map(|(_, super_method)| { Self::generate_super_method(target_scope_id, super_method, ctx) })); - declarations - } else { - ctx.ast.vec_with_capacity(1) } - } else { - ctx.ast.vec_with_capacity(1) - }; + } // `_this = this;` if let Some(this_var) = this_var { @@ -823,10 +1027,7 @@ impl<'a> ArrowFunctionConverter<'a> { declarations.push(variable_declarator); } - // If there are no declarations, we don't need to insert a variable declaration. - if declarations.is_empty() { - return; - } + debug_assert_eq!(declarations_count, declarations.len()); let stmt = ctx.ast.alloc_variable_declaration( SPAN, diff --git a/crates/oxc_transformer/src/common/mod.rs b/crates/oxc_transformer/src/common/mod.rs index 8fc341179f10c..6c372b4a52f19 100644 --- a/crates/oxc_transformer/src/common/mod.rs +++ b/crates/oxc_transformer/src/common/mod.rs @@ -103,4 +103,20 @@ impl<'a, 'ctx> Traverse<'a> for Common<'a, '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/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 474564f4339b0..1ff1d32c6620e 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -190,6 +190,22 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { self.x2_es2020.enter_big_int_literal(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); diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index 1078e19e21a03..1cc7879ddaed1 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: d20b314c -Passed: 82/92 +Passed: 85/95 # All Passed: * babel-plugin-transform-class-static-block diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/assign/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/assign/input.js new file mode 100644 index 0000000000000..5b43966205ad8 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/assign/input.js @@ -0,0 +1,4 @@ +const ArgumentsAssignment = async () => { + let arguments = arguments; + console.log(arguments); +}; diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/assign/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/assign/output.js new file mode 100644 index 0000000000000..097d15571fd88 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/assign/output.js @@ -0,0 +1,9 @@ +const ArgumentsAssignment = /*#__PURE__*/function () { + var _ref = babelHelpers.asyncToGenerator(function* () { + let _arguments = _arguments; + console.log(_arguments); + }); + return function ArgumentsAssignment() { + return _ref.apply(this, arguments); + }; +}(); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/async-method/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/async-method/input.js new file mode 100644 index 0000000000000..eb6b94e8fc8e3 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/async-method/input.js @@ -0,0 +1,7 @@ +class Cls { + async method() { + () => { + console.log(arguments); + } + } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/async-method/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/async-method/output.js new file mode 100644 index 0000000000000..5650ed3766c7a --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/async-method/output.js @@ -0,0 +1,10 @@ +class Cls { + method() { + var _arguments = arguments; + return babelHelpers.asyncToGenerator(function* () { + () => { + console.log(_arguments); + }; + })(); + } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/nested-block/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/nested-block/input.js new file mode 100644 index 0000000000000..640d39129b23a --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/nested-block/input.js @@ -0,0 +1,7 @@ +const ArrowFunction = async () => { + { + var arguments = arguments; + console.log(arguments); + } + console.log(arguments); +}; diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/nested-block/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/nested-block/output.js new file mode 100644 index 0000000000000..1d962da7d59c9 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arguments/nested-block/output.js @@ -0,0 +1,12 @@ +const ArrowFunction = /*#__PURE__*/function () { + var _ref = babelHelpers.asyncToGenerator(function* () { + { + var _arguments = _arguments; + console.log(_arguments); + } + console.log(_arguments); + }); + return function ArrowFunction() { + return _ref.apply(this, arguments); + }; +}();