mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
refactor(transformer): move implementation of ArrowFunction to common/ArrowFunctionConverter (#7107)
Part of #7074 In order can reuse the ability of the `ArrowFunction` plugin, we moved out the implementation to common, then we can use use in other plugins
This commit is contained in:
parent
e04ee97870
commit
ff8bd50d38
5 changed files with 533 additions and 365 deletions
439
crates/oxc_transformer/src/common/arrow_function_converter.rs
Normal file
439
crates/oxc_transformer/src/common/arrow_function_converter.rs
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
//! 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
|
||||
//! <https://github.com/babel/babel/blob/d20b314c14533ab86351ecf6ca6b7296b66a57b3/packages/babel-traverse/src/path/conversion.ts#L170-L247>
|
||||
|
||||
use oxc_allocator::{Box as ArenaBox, Vec as ArenaVec};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_data_structures::stack::SparseStack;
|
||||
use oxc_span::SPAN;
|
||||
use oxc_syntax::{
|
||||
scope::{ScopeFlags, ScopeId},
|
||||
symbol::SymbolFlags,
|
||||
};
|
||||
use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx};
|
||||
|
||||
/// 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
|
||||
#[expect(unused)]
|
||||
AsyncOnly,
|
||||
}
|
||||
|
||||
pub struct ArrowFunctionConverterOptions {
|
||||
pub mode: ArrowFunctionConverterMode,
|
||||
}
|
||||
|
||||
pub struct ArrowFunctionConverter<'a> {
|
||||
mode: ArrowFunctionConverterMode,
|
||||
this_var_stack: SparseStack<BoundIdentifier<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> ArrowFunctionConverter<'a> {
|
||||
pub fn new(options: &ArrowFunctionConverterOptions) -> Self {
|
||||
// `SparseStack` is created with 1 empty entry, for `Program`
|
||||
Self { mode: options.mode, this_var_stack: SparseStack::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> {
|
||||
// Note: No visitors for `TSModuleBlock` because `this` is not legal in TS module blocks.
|
||||
// <https://www.typescriptlang.org/play/?#code/HYQwtgpgzgDiDGEAEAxA9mpBvAsAKCSXjWCgBckANJAXiQAoBKWgPiTIAsBLKAbnwC++fGDQATAK4AbZACEQAJ2z5CxUhWp0mrdtz6D8QA>
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
if let Some(this_var) = self.this_var_stack.take_last() {
|
||||
self.insert_this_var_statement_at_the_top_of_statements(
|
||||
&mut program.body,
|
||||
&this_var,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
debug_assert!(self.this_var_stack.len() == 1);
|
||||
debug_assert!(self.this_var_stack.last().is_none());
|
||||
}
|
||||
|
||||
fn enter_function(&mut self, _func: &mut Function<'a>, _ctx: &mut TraverseCtx<'a>) {
|
||||
if self.is_disabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.this_var_stack.push(None);
|
||||
}
|
||||
|
||||
/// ```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;
|
||||
}
|
||||
|
||||
if let Some(this_var) = self.this_var_stack.pop() {
|
||||
let Some(body) = &mut func.body else { unreachable!() };
|
||||
|
||||
self.insert_this_var_statement_at_the_top_of_statements(
|
||||
&mut body.statements,
|
||||
&this_var,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.is_disabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(this_var) = self.this_var_stack.pop() {
|
||||
self.insert_this_var_statement_at_the_top_of_statements(
|
||||
&mut block.body,
|
||||
&this_var,
|
||||
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 {
|
||||
if 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 {
|
||||
if 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;
|
||||
}
|
||||
|
||||
if let Expression::ThisExpression(this) = expr {
|
||||
if let Some(ident) = self.get_this_identifier(this.span, ctx) {
|
||||
*expr = Expression::Identifier(ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.is_disabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Expression::ArrowFunctionExpression(_) = expr {
|
||||
let Expression::ArrowFunctionExpression(arrow_function_expr) =
|
||||
ctx.ast.move_expression(expr)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
*expr = self.transform_arrow_function_expression(arrow_function_expr.unbox(), ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ArrowFunctionConverter<'a> {
|
||||
/// Check if arrow function conversion is disabled
|
||||
fn is_disabled(&self) -> bool {
|
||||
self.mode == ArrowFunctionConverterMode::Disabled
|
||||
}
|
||||
|
||||
fn get_this_identifier(
|
||||
&mut self,
|
||||
span: Span,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) -> Option<ArenaBox<'a, IdentifierReference<'a>>> {
|
||||
// Find arrow function we are currently in (if we are)
|
||||
let arrow_scope_id = Self::get_arrow_function_scope(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.
|
||||
// <https://github.com/oxc-project/oxc/pull/5840>
|
||||
let this_var = self.this_var_stack.last_or_init(|| {
|
||||
let target_scope_id = ctx
|
||||
.scopes()
|
||||
.ancestors(arrow_scope_id)
|
||||
// Skip arrow function scope
|
||||
.skip(1)
|
||||
.find(|&scope_id| {
|
||||
let scope_flags = ctx.scopes().get_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)
|
||||
});
|
||||
Some(ctx.ast.alloc(this_var.create_spanned_read_reference(span, ctx)))
|
||||
}
|
||||
|
||||
/// Find arrow function we are currently in, if it's between current node, and where `this` is bound.
|
||||
/// Return its `ScopeId`.
|
||||
fn get_arrow_function_scope(ctx: &mut TraverseCtx<'a>) -> Option<ScopeId> {
|
||||
// `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.
|
||||
for ancestor in ctx.ancestors() {
|
||||
match ancestor {
|
||||
// Top level
|
||||
Ancestor::ProgramBody(_)
|
||||
// Function (includes class method body)
|
||||
| Ancestor::FunctionParams(_)
|
||||
| Ancestor::FunctionBody(_)
|
||||
// Class property body
|
||||
| Ancestor::PropertyDefinitionValue(_)
|
||||
// Class accessor property body
|
||||
| Ancestor::AccessorPropertyValue(_)
|
||||
// Class static block
|
||||
| Ancestor::StaticBlockBody(_) => return None,
|
||||
// Arrow function
|
||||
Ancestor::ArrowFunctionExpressionParams(func) => {
|
||||
return Some(func.scope_id().get().unwrap())
|
||||
}
|
||||
Ancestor::ArrowFunctionExpressionBody(func) => {
|
||||
return Some(func.scope_id().get().unwrap())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
#[expect(clippy::unused_self)]
|
||||
fn transform_arrow_function_expression(
|
||||
&mut self,
|
||||
arrow_function_expr: ArrowFunctionExpression<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) -> Expression<'a> {
|
||||
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);
|
||||
}
|
||||
|
||||
let scope_id = arrow_function_expr.scope_id.get().unwrap();
|
||||
let flags = ctx.scopes_mut().get_flags_mut(scope_id);
|
||||
*flags &= !ScopeFlags::Arrow;
|
||||
|
||||
Expression::FunctionExpression(ctx.ast.alloc_function_with_scope_id(
|
||||
FunctionType::FunctionExpression,
|
||||
arrow_function_expr.span,
|
||||
None,
|
||||
false,
|
||||
arrow_function_expr.r#async,
|
||||
false,
|
||||
arrow_function_expr.type_parameters,
|
||||
None::<TSThisParameter<'a>>,
|
||||
arrow_function_expr.params,
|
||||
arrow_function_expr.return_type,
|
||||
Some(body),
|
||||
scope_id,
|
||||
))
|
||||
}
|
||||
|
||||
/// Insert `var _this = this;` at the top of the statements.
|
||||
#[expect(clippy::unused_self)]
|
||||
fn insert_this_var_statement_at_the_top_of_statements(
|
||||
&mut self,
|
||||
statements: &mut ArenaVec<'a, Statement<'a>>,
|
||||
this_var: &BoundIdentifier<'a>,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) {
|
||||
let variable_declarator = ctx.ast.variable_declarator(
|
||||
SPAN,
|
||||
VariableDeclarationKind::Var,
|
||||
this_var.create_binding_pattern(ctx),
|
||||
Some(ctx.ast.expression_this(SPAN)),
|
||||
false,
|
||||
);
|
||||
|
||||
let stmt = ctx.ast.alloc_variable_declaration(
|
||||
SPAN,
|
||||
VariableDeclarationKind::Var,
|
||||
ctx.ast.vec1(variable_declarator),
|
||||
false,
|
||||
);
|
||||
|
||||
let stmt = Statement::VariableDeclaration(stmt);
|
||||
|
||||
statements.insert(0, stmt);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
//! Utility transforms which are in common between other transforms.
|
||||
|
||||
use arrow_function_converter::{
|
||||
ArrowFunctionConverter, ArrowFunctionConverterMode, ArrowFunctionConverterOptions,
|
||||
};
|
||||
use oxc_allocator::Vec as ArenaVec;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_traverse::{Traverse, TraverseCtx};
|
||||
|
||||
use crate::TransformCtx;
|
||||
use crate::{TransformCtx, TransformOptions};
|
||||
|
||||
pub mod arrow_function_converter;
|
||||
pub mod helper_loader;
|
||||
pub mod module_imports;
|
||||
pub mod statement_injector;
|
||||
|
|
@ -22,15 +26,28 @@ pub struct Common<'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(ctx: &'ctx TransformCtx<'a>) -> Self {
|
||||
pub fn new(options: &TransformOptions, ctx: &'ctx TransformCtx<'a>) -> Self {
|
||||
let arrow_function_converter_options = {
|
||||
let mode = if options.env.es2015.arrow_function.is_some() {
|
||||
ArrowFunctionConverterMode::Enabled
|
||||
} else {
|
||||
ArrowFunctionConverterMode::Disabled
|
||||
};
|
||||
ArrowFunctionConverterOptions { mode }
|
||||
};
|
||||
|
||||
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(
|
||||
&arrow_function_converter_options,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +57,7 @@ impl<'a, 'ctx> Traverse<'a> for Common<'a, 'ctx> {
|
|||
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);
|
||||
}
|
||||
|
||||
fn enter_statements(
|
||||
|
|
@ -58,4 +76,44 @@ impl<'a, 'ctx> Traverse<'a> for Common<'a, 'ctx> {
|
|||
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_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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,8 @@
|
|||
//!
|
||||
//! ## Implementation
|
||||
//!
|
||||
//! Implementation based on [@babel/plugin-transform-arrow-functions](https://babel.dev/docs/babel-plugin-transform-arrow-functions).
|
||||
//! The implementation is placed in [`crate::common::arrow_function_converter::ArrowFunctionConverter`],
|
||||
//! which can be used in other plugins.
|
||||
//!
|
||||
//! ## References:
|
||||
//!
|
||||
|
|
@ -126,15 +127,9 @@
|
|||
|
||||
use serde::Deserialize;
|
||||
|
||||
use oxc_allocator::{Box as ArenaBox, Vec as ArenaVec};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_data_structures::stack::SparseStack;
|
||||
use oxc_span::SPAN;
|
||||
use oxc_syntax::{
|
||||
scope::{ScopeFlags, ScopeId},
|
||||
symbol::SymbolFlags,
|
||||
};
|
||||
use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx};
|
||||
use oxc_traverse::Traverse;
|
||||
|
||||
use crate::context::TransformCtx;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize)]
|
||||
pub struct ArrowFunctionsOptions {
|
||||
|
|
@ -146,284 +141,15 @@ pub struct ArrowFunctionsOptions {
|
|||
pub spec: bool,
|
||||
}
|
||||
|
||||
pub struct ArrowFunctions<'a> {
|
||||
pub struct ArrowFunctions<'a, 'ctx> {
|
||||
_options: ArrowFunctionsOptions,
|
||||
this_var_stack: SparseStack<BoundIdentifier<'a>>,
|
||||
_ctx: &'ctx TransformCtx<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ArrowFunctions<'a> {
|
||||
pub fn new(options: ArrowFunctionsOptions) -> Self {
|
||||
// `SparseStack` is created with 1 empty entry, for `Program`
|
||||
Self { _options: options, this_var_stack: SparseStack::new() }
|
||||
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> for ArrowFunctions<'a> {
|
||||
// Note: No visitors for `TSModuleBlock` because `this` is not legal in TS module blocks.
|
||||
// <https://www.typescriptlang.org/play/?#code/HYQwtgpgzgDiDGEAEAxA9mpBvAsAKCSXjWCgBckANJAXiQAoBKWgPiTIAsBLKAbnwC++fGDQATAK4AbZACEQAJ2z5CxUhWp0mrdtz6D8QA>
|
||||
|
||||
/// Insert `var _this = this;` for the global scope.
|
||||
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if let Some(this_var) = self.this_var_stack.take_last() {
|
||||
self.insert_this_var_statement_at_the_top_of_statements(
|
||||
&mut program.body,
|
||||
&this_var,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
debug_assert!(self.this_var_stack.len() == 1);
|
||||
debug_assert!(self.this_var_stack.last().is_none());
|
||||
}
|
||||
|
||||
fn enter_function(&mut self, _func: &mut Function<'a>, _ctx: &mut TraverseCtx<'a>) {
|
||||
self.this_var_stack.push(None);
|
||||
}
|
||||
|
||||
/// ```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 let Some(this_var) = self.this_var_stack.pop() {
|
||||
let Some(body) = &mut func.body else { unreachable!() };
|
||||
|
||||
self.insert_this_var_statement_at_the_top_of_statements(
|
||||
&mut body.statements,
|
||||
&this_var,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_static_block(&mut self, _block: &mut StaticBlock<'a>, _ctx: &mut TraverseCtx<'a>) {
|
||||
self.this_var_stack.push(None);
|
||||
}
|
||||
|
||||
fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if let Some(this_var) = self.this_var_stack.pop() {
|
||||
self.insert_this_var_statement_at_the_top_of_statements(
|
||||
&mut block.body,
|
||||
&this_var,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_jsx_element_name(
|
||||
&mut self,
|
||||
element_name: &mut JSXElementName<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) {
|
||||
if let JSXElementName::ThisExpression(this) = element_name {
|
||||
if 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 let JSXMemberExpressionObject::ThisExpression(this) = object {
|
||||
if 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 let Expression::ThisExpression(this) = expr {
|
||||
if let Some(ident) = self.get_this_identifier(this.span, ctx) {
|
||||
*expr = Expression::Identifier(ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if let Expression::ArrowFunctionExpression(_) = expr {
|
||||
let Expression::ArrowFunctionExpression(arrow_function_expr) =
|
||||
ctx.ast.move_expression(expr)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
*expr = self.transform_arrow_function_expression(arrow_function_expr.unbox(), ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ArrowFunctions<'a> {
|
||||
fn get_this_identifier(
|
||||
&mut self,
|
||||
span: Span,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) -> Option<ArenaBox<'a, IdentifierReference<'a>>> {
|
||||
// Find arrow function we are currently in (if we are)
|
||||
let arrow_scope_id = Self::get_arrow_function_scope(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.
|
||||
// <https://github.com/oxc-project/oxc/pull/5840>
|
||||
let this_var = self.this_var_stack.last_or_init(|| {
|
||||
let target_scope_id = ctx
|
||||
.scopes()
|
||||
.ancestors(arrow_scope_id)
|
||||
// Skip arrow function scope
|
||||
.skip(1)
|
||||
.find(|&scope_id| {
|
||||
let scope_flags = ctx.scopes().get_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)
|
||||
});
|
||||
Some(ctx.ast.alloc(this_var.create_spanned_read_reference(span, ctx)))
|
||||
}
|
||||
|
||||
/// Find arrow function we are currently in, if it's between current node, and where `this` is bound.
|
||||
/// Return its `ScopeId`.
|
||||
fn get_arrow_function_scope(ctx: &mut TraverseCtx<'a>) -> Option<ScopeId> {
|
||||
// `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.
|
||||
for ancestor in ctx.ancestors() {
|
||||
match ancestor {
|
||||
// Top level
|
||||
Ancestor::ProgramBody(_)
|
||||
// Function (includes class method body)
|
||||
| Ancestor::FunctionParams(_)
|
||||
| Ancestor::FunctionBody(_)
|
||||
// Class property body
|
||||
| Ancestor::PropertyDefinitionValue(_)
|
||||
// Class accessor property body
|
||||
| Ancestor::AccessorPropertyValue(_)
|
||||
// Class static block
|
||||
| Ancestor::StaticBlockBody(_) => return None,
|
||||
// Arrow function
|
||||
Ancestor::ArrowFunctionExpressionParams(func) => {
|
||||
return Some(func.scope_id().get().unwrap())
|
||||
}
|
||||
Ancestor::ArrowFunctionExpressionBody(func) => {
|
||||
return Some(func.scope_id().get().unwrap())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
#[expect(clippy::unused_self)]
|
||||
fn transform_arrow_function_expression(
|
||||
&mut self,
|
||||
arrow_function_expr: ArrowFunctionExpression<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) -> Expression<'a> {
|
||||
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);
|
||||
}
|
||||
|
||||
let scope_id = arrow_function_expr.scope_id.get().unwrap();
|
||||
let flags = ctx.scopes_mut().get_flags_mut(scope_id);
|
||||
*flags &= !ScopeFlags::Arrow;
|
||||
|
||||
Expression::FunctionExpression(ctx.ast.alloc_function_with_scope_id(
|
||||
FunctionType::FunctionExpression,
|
||||
arrow_function_expr.span,
|
||||
None,
|
||||
false,
|
||||
arrow_function_expr.r#async,
|
||||
false,
|
||||
arrow_function_expr.type_parameters,
|
||||
None::<TSThisParameter<'a>>,
|
||||
arrow_function_expr.params,
|
||||
arrow_function_expr.return_type,
|
||||
Some(body),
|
||||
scope_id,
|
||||
))
|
||||
}
|
||||
|
||||
/// Insert `var _this = this;` at the top of the statements.
|
||||
#[expect(clippy::unused_self)]
|
||||
fn insert_this_var_statement_at_the_top_of_statements(
|
||||
&mut self,
|
||||
statements: &mut ArenaVec<'a, Statement<'a>>,
|
||||
this_var: &BoundIdentifier<'a>,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) {
|
||||
let variable_declarator = ctx.ast.variable_declarator(
|
||||
SPAN,
|
||||
VariableDeclarationKind::Var,
|
||||
this_var.create_binding_pattern(ctx),
|
||||
Some(ctx.ast.expression_this(SPAN)),
|
||||
false,
|
||||
);
|
||||
|
||||
let stmt = ctx.ast.alloc_variable_declaration(
|
||||
SPAN,
|
||||
VariableDeclarationKind::Var,
|
||||
ctx.ast.vec1(variable_declarator),
|
||||
false,
|
||||
);
|
||||
|
||||
let stmt = Statement::VariableDeclaration(stmt);
|
||||
|
||||
statements.insert(0, stmt);
|
||||
}
|
||||
}
|
||||
impl<'a, 'ctx> Traverse<'a> for ArrowFunctions<'a, 'ctx> {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use oxc_ast::ast::*;
|
||||
use oxc_traverse::{Traverse, TraverseCtx};
|
||||
use oxc_traverse::Traverse;
|
||||
|
||||
mod arrow_functions;
|
||||
mod options;
|
||||
|
|
@ -7,78 +6,24 @@ mod options;
|
|||
pub use arrow_functions::{ArrowFunctions, ArrowFunctionsOptions};
|
||||
pub use options::ES2015Options;
|
||||
|
||||
pub struct ES2015<'a> {
|
||||
use crate::context::TransformCtx;
|
||||
|
||||
pub struct ES2015<'a, 'ctx> {
|
||||
#[expect(unused)]
|
||||
options: ES2015Options,
|
||||
|
||||
// Plugins
|
||||
arrow_functions: ArrowFunctions<'a>,
|
||||
#[expect(unused)]
|
||||
arrow_functions: ArrowFunctions<'a, 'ctx>,
|
||||
}
|
||||
|
||||
impl<'a> ES2015<'a> {
|
||||
pub fn new(options: ES2015Options) -> Self {
|
||||
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()),
|
||||
arrow_functions: ArrowFunctions::new(options.arrow_function.unwrap_or_default(), ctx),
|
||||
options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Traverse<'a> for ES2015<'a> {
|
||||
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.exit_program(program, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.enter_function(func, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.exit_function(func, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.enter_expression(expr, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.exit_expression(expr, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.enter_static_block(block, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.exit_static_block(block, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_jsx_element_name(&mut self, node: &mut JSXElementName<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.enter_jsx_element_name(node, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_jsx_member_expression_object(
|
||||
&mut self,
|
||||
node: &mut JSXMemberExpressionObject<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) {
|
||||
if self.options.arrow_function.is_some() {
|
||||
self.arrow_functions.enter_jsx_member_expression_object(node, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a, 'ctx> Traverse<'a> for ES2015<'a, 'ctx> {}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ impl<'a> Transformer<'a> {
|
|||
jsx::update_options_with_comments(&program.comments, &mut self.options, &self.ctx);
|
||||
|
||||
let mut transformer = TransformerImpl {
|
||||
common: Common::new(&self.options, &self.ctx),
|
||||
x0_typescript: program
|
||||
.source_type
|
||||
.is_typescript()
|
||||
|
|
@ -106,9 +107,8 @@ impl<'a> Transformer<'a> {
|
|||
x2_es2018: ES2018::new(self.options.env.es2018, &self.ctx),
|
||||
x2_es2016: ES2016::new(self.options.env.es2016, &self.ctx),
|
||||
x2_es2017: ES2017::new(self.options.env.es2017, &self.ctx),
|
||||
x3_es2015: ES2015::new(self.options.env.es2015),
|
||||
x3_es2015: ES2015::new(self.options.env.es2015, &self.ctx),
|
||||
x4_regexp: RegExp::new(self.options.env.regexp, &self.ctx),
|
||||
common: Common::new(&self.ctx),
|
||||
};
|
||||
|
||||
let (symbols, scopes) = traverse_mut(&mut transformer, allocator, program, symbols, scopes);
|
||||
|
|
@ -127,7 +127,8 @@ struct TransformerImpl<'a, 'ctx> {
|
|||
x2_es2018: ES2018<'a, 'ctx>,
|
||||
x2_es2017: ES2017<'a, 'ctx>,
|
||||
x2_es2016: ES2016<'a, 'ctx>,
|
||||
x3_es2015: ES2015<'a>,
|
||||
#[expect(unused)]
|
||||
x3_es2015: ES2015<'a, 'ctx>,
|
||||
x4_regexp: RegExp<'a, 'ctx>,
|
||||
common: Common<'a, 'ctx>,
|
||||
}
|
||||
|
|
@ -145,7 +146,6 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
|
|||
if let Some(typescript) = self.x0_typescript.as_mut() {
|
||||
typescript.exit_program(program, ctx);
|
||||
}
|
||||
self.x3_es2015.exit_program(program, ctx);
|
||||
self.common.exit_program(program, ctx);
|
||||
}
|
||||
|
||||
|
|
@ -198,11 +198,11 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
|
|||
}
|
||||
|
||||
fn enter_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
self.x3_es2015.enter_static_block(block, ctx);
|
||||
self.common.enter_static_block(block, ctx);
|
||||
}
|
||||
|
||||
fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
self.x3_es2015.exit_static_block(block, ctx);
|
||||
self.common.exit_static_block(block, ctx);
|
||||
}
|
||||
|
||||
fn enter_ts_module_declaration(
|
||||
|
|
@ -224,15 +224,15 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
|
|||
self.x2_es2020.enter_expression(expr, ctx);
|
||||
self.x2_es2018.enter_expression(expr, ctx);
|
||||
self.x2_es2016.enter_expression(expr, ctx);
|
||||
self.x3_es2015.enter_expression(expr, ctx);
|
||||
self.x4_regexp.enter_expression(expr, ctx);
|
||||
self.common.enter_expression(expr, ctx);
|
||||
}
|
||||
|
||||
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
self.x1_jsx.exit_expression(expr, ctx);
|
||||
self.x2_es2018.exit_expression(expr, ctx);
|
||||
self.x2_es2017.exit_expression(expr, ctx);
|
||||
self.x3_es2015.exit_expression(expr, ctx);
|
||||
self.common.exit_expression(expr, ctx);
|
||||
}
|
||||
|
||||
fn enter_simple_assignment_target(
|
||||
|
|
@ -267,7 +267,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
|
|||
|
||||
fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
self.x2_es2018.enter_function(func, ctx);
|
||||
self.x3_es2015.enter_function(func, ctx);
|
||||
self.common.enter_function(func, ctx);
|
||||
}
|
||||
|
||||
fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
|
|
@ -277,7 +277,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
|
|||
self.x1_jsx.exit_function(func, ctx);
|
||||
self.x2_es2018.exit_function(func, ctx);
|
||||
self.x2_es2017.exit_function(func, ctx);
|
||||
self.x3_es2015.exit_function(func, ctx);
|
||||
self.common.exit_function(func, ctx);
|
||||
}
|
||||
|
||||
fn enter_jsx_element(&mut self, node: &mut JSXElement<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
|
|
@ -287,7 +287,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
|
|||
}
|
||||
|
||||
fn enter_jsx_element_name(&mut self, node: &mut JSXElementName<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
self.x3_es2015.enter_jsx_element_name(node, ctx);
|
||||
self.common.enter_jsx_element_name(node, ctx);
|
||||
}
|
||||
|
||||
fn enter_jsx_member_expression_object(
|
||||
|
|
@ -295,7 +295,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
|
|||
node: &mut JSXMemberExpressionObject<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) {
|
||||
self.x3_es2015.enter_jsx_member_expression_object(node, ctx);
|
||||
self.common.enter_jsx_member_expression_object(node, ctx);
|
||||
}
|
||||
|
||||
fn enter_jsx_fragment(&mut self, node: &mut JSXFragment<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue