mirror of
https://github.com/danbulant/oxc
synced 2026-05-22 21:58:36 +00:00
fix(transformer/class-properties): fix symbol clashes in instance prop initializers (#7872)
Instance property initializers are moved into constructor. If symbols they reference are shadowed within constructor, rename those symbols.
Input:
```js
class C {
prop = foo();
constructor(foo) {
console.log(foo);
}
}
```
Output:
```js
class C {
constructor(_foo) { // <-- renamed
this.prop = foo(); // <-- moved into constructor
console.log(_foo); // <-- renamed
}
}
```
This commit is contained in:
parent
f0a8d8aab7
commit
e76fbb0721
6 changed files with 227 additions and 93 deletions
|
|
@ -411,12 +411,23 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
// Existing constructor
|
// Existing constructor
|
||||||
let constructor = constructor.value.as_mut();
|
let constructor = constructor.value.as_mut();
|
||||||
if class.super_class.is_some() {
|
if class.super_class.is_some() {
|
||||||
let (instance_inits_scope_id, insert_location) =
|
let (insert_scopes, insert_location) =
|
||||||
Self::replace_super_in_constructor(constructor, ctx);
|
Self::replace_super_in_constructor(constructor, ctx);
|
||||||
self.instance_inits_scope_id = instance_inits_scope_id;
|
self.instance_inits_scope_id = insert_scopes.insert_in_scope_id;
|
||||||
|
self.instance_inits_constructor_scope_id = insert_scopes.constructor_scope_id;
|
||||||
Some(insert_location)
|
Some(insert_location)
|
||||||
} else {
|
} else {
|
||||||
self.instance_inits_scope_id = constructor.scope_id();
|
let constructor_scope_id = constructor.scope_id();
|
||||||
|
self.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.
|
||||||
|
self.instance_inits_constructor_scope_id =
|
||||||
|
if ctx.scopes().get_bindings(constructor_scope_id).is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(constructor_scope_id)
|
||||||
|
};
|
||||||
Some(InstanceInitsInsertLocation::ExistingConstructor(0))
|
Some(InstanceInitsInsertLocation::ExistingConstructor(0))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -427,6 +438,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
ScopeFlags::Function | ScopeFlags::Constructor | ScopeFlags::StrictMode,
|
ScopeFlags::Function | ScopeFlags::Constructor | ScopeFlags::StrictMode,
|
||||||
);
|
);
|
||||||
self.instance_inits_scope_id = constructor_scope_id;
|
self.instance_inits_scope_id = constructor_scope_id;
|
||||||
|
self.instance_inits_constructor_scope_id = None;
|
||||||
Some(InstanceInitsInsertLocation::NewConstructor)
|
Some(InstanceInitsInsertLocation::NewConstructor)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,13 +99,14 @@
|
||||||
//! ESBuild does not handle `super()` in constructor params correctly:
|
//! ESBuild does not handle `super()` in constructor params correctly:
|
||||||
//! [ESBuild REPL](https://esbuild.github.io/try/#dAAwLjI0LjAALS10YXJnZXQ9ZXMyMDIwAGNsYXNzIEMgZXh0ZW5kcyBTIHsKICBwcm9wID0gZm9vKCk7CiAgY29uc3RydWN0b3IoeCA9IHN1cGVyKCksIHkgPSBzdXBlcigpKSB7fQp9Cg)
|
//! [ESBuild REPL](https://esbuild.github.io/try/#dAAwLjI0LjAALS10YXJnZXQ9ZXMyMDIwAGNsYXNzIEMgZXh0ZW5kcyBTIHsKICBwcm9wID0gZm9vKCk7CiAgY29uc3RydWN0b3IoeCA9IHN1cGVyKCksIHkgPSBzdXBlcigpKSB7fQp9Cg)
|
||||||
|
|
||||||
use oxc_allocator::Vec as ArenaVec;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use oxc_ast::{ast::*, visit::walk_mut, VisitMut, NONE};
|
use oxc_ast::{ast::*, visit::walk_mut, VisitMut, NONE};
|
||||||
use oxc_span::SPAN;
|
use oxc_span::SPAN;
|
||||||
use oxc_syntax::{
|
use oxc_syntax::{
|
||||||
node::NodeId,
|
node::NodeId,
|
||||||
scope::{ScopeFlags, ScopeId},
|
scope::{ScopeFlags, ScopeId},
|
||||||
symbol::SymbolFlags,
|
symbol::{SymbolFlags, SymbolId},
|
||||||
};
|
};
|
||||||
use oxc_traverse::{BoundIdentifier, TraverseCtx};
|
use oxc_traverse::{BoundIdentifier, TraverseCtx};
|
||||||
|
|
||||||
|
|
@ -126,11 +127,23 @@ pub(super) enum InstanceInitsInsertLocation<'a> {
|
||||||
SuperFnOutsideClass(BoundIdentifier<'a>),
|
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<ScopeId>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
/// Replace `super()` call(s) in constructor, if required.
|
/// Replace `super()` call(s) in constructor, if required.
|
||||||
///
|
///
|
||||||
/// Returns `InstanceInitsInsertLocation` detailing where instance property initializers
|
/// Returns:
|
||||||
/// should be inserted.
|
/// * `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):
|
/// * `super()` first appears as a top level statement in constructor body (common case):
|
||||||
/// * Do not alter constructor.
|
/// * Do not alter constructor.
|
||||||
|
|
@ -151,28 +164,50 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
///
|
///
|
||||||
/// See doc comment at top of this file for more details of last 3 cases.
|
/// See doc comment at top of this file for more details of last 3 cases.
|
||||||
///
|
///
|
||||||
/// If a `_super` function is required, binding for `_super`, and `ScopeId` of `_super` function
|
/// If a `_super` function is required, binding for `_super` is recorded in the returned
|
||||||
/// are recorded in the returned `InstanceInitsInsertLocation`.
|
/// `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.
|
/// This function does not create the `_super` function or insert it. That happens later.
|
||||||
pub(super) fn replace_super_in_constructor(
|
pub(super) fn replace_super_in_constructor(
|
||||||
constructor: &mut Function<'a>,
|
constructor: &mut Function<'a>,
|
||||||
ctx: &mut TraverseCtx<'a>,
|
ctx: &mut TraverseCtx<'a>,
|
||||||
) -> (ScopeId, InstanceInitsInsertLocation<'a>) {
|
) -> (InstanceInitScopes, InstanceInitsInsertLocation<'a>) {
|
||||||
// Find any `super()`s in constructor params and replace with `_super.call(super())`
|
// Find any `super()`s in constructor params and replace with `_super.call(super())`
|
||||||
let replacer = ConstructorParamsSuperReplacer::new(ctx);
|
let replacer = ConstructorParamsSuperReplacer::new(ctx);
|
||||||
if let Some(result) = replacer.replace(constructor) {
|
if let Some((super_func_scope_id, insert_location)) = replacer.replace(constructor) {
|
||||||
return result;
|
// `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.
|
// No `super()` in constructor params.
|
||||||
// Insert property initializers after `super()` statement, or in a `_super` function.
|
// Property initializers will be inserted after `super()` statement,
|
||||||
let replacer = ConstructorBodySuperReplacer::new(constructor.scope_id(), ctx);
|
// or in a `_super` function inserted at top of constructor.
|
||||||
let body_stmts = &mut constructor.body.as_mut().unwrap().statements;
|
let constructor_scope_id = constructor.scope_id();
|
||||||
replacer.replace(body_stmts)
|
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.scopes().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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert property initializers into existing class constructor.
|
/// Insert instance property initializers.
|
||||||
///
|
///
|
||||||
/// `scope_id` has different meaning depending on type of `insertion_location`.
|
/// `scope_id` has different meaning depending on type of `insertion_location`.
|
||||||
pub(super) fn insert_instance_inits(
|
pub(super) fn insert_instance_inits(
|
||||||
|
|
@ -193,7 +228,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
Self::insert_constructor(class, scope_id, inits, ctx);
|
Self::insert_constructor(class, scope_id, inits, ctx);
|
||||||
}
|
}
|
||||||
InstanceInitsInsertLocation::ExistingConstructor(stmt_index) => {
|
InstanceInitsInsertLocation::ExistingConstructor(stmt_index) => {
|
||||||
Self::insert_inits_into_constructor_as_statements(
|
self.insert_inits_into_constructor_as_statements(
|
||||||
class,
|
class,
|
||||||
inits,
|
inits,
|
||||||
constructor_index,
|
constructor_index,
|
||||||
|
|
@ -202,7 +237,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding) => {
|
InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding) => {
|
||||||
Self::create_super_function_inside_constructor(
|
self.create_super_function_inside_constructor(
|
||||||
class,
|
class,
|
||||||
inits,
|
inits,
|
||||||
super_binding,
|
super_binding,
|
||||||
|
|
@ -282,13 +317,19 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
|
|
||||||
/// Insert instance property initializers into constructor body at `insertion_index`.
|
/// Insert instance property initializers into constructor body at `insertion_index`.
|
||||||
fn insert_inits_into_constructor_as_statements(
|
fn insert_inits_into_constructor_as_statements(
|
||||||
|
&mut self,
|
||||||
class: &mut Class<'a>,
|
class: &mut Class<'a>,
|
||||||
inits: Vec<Expression<'a>>,
|
inits: Vec<Expression<'a>>,
|
||||||
constructor_index: usize,
|
constructor_index: usize,
|
||||||
insertion_index: usize,
|
insertion_index: usize,
|
||||||
ctx: &TraverseCtx<'a>,
|
ctx: &mut TraverseCtx<'a>,
|
||||||
) {
|
) {
|
||||||
let body_stmts = Self::get_constructor_body_stmts(class, constructor_index);
|
// Rename any symbols in constructor which clash with references in inits
|
||||||
|
let constructor = Self::get_constructor(class, constructor_index);
|
||||||
|
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));
|
body_stmts.splice(insertion_index..insertion_index, exprs_into_stmts(inits, ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,6 +337,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
/// and insert at top of constructor body.
|
/// and insert at top of constructor body.
|
||||||
/// `var _super = (..._args) => (super(..._args), <inits>, this);`
|
/// `var _super = (..._args) => (super(..._args), <inits>, this);`
|
||||||
fn create_super_function_inside_constructor(
|
fn create_super_function_inside_constructor(
|
||||||
|
&mut self,
|
||||||
class: &mut Class<'a>,
|
class: &mut Class<'a>,
|
||||||
inits: Vec<Expression<'a>>,
|
inits: Vec<Expression<'a>>,
|
||||||
super_binding: &BoundIdentifier<'a>,
|
super_binding: &BoundIdentifier<'a>,
|
||||||
|
|
@ -303,6 +345,10 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
constructor_index: usize,
|
constructor_index: usize,
|
||||||
ctx: &mut TraverseCtx<'a>,
|
ctx: &mut TraverseCtx<'a>,
|
||||||
) {
|
) {
|
||||||
|
// Rename any symbols in constructor which clash with references in inits
|
||||||
|
let constructor = Self::get_constructor(class, constructor_index);
|
||||||
|
self.rename_clashing_symbols(constructor, ctx);
|
||||||
|
|
||||||
// `(super(..._args), <inits>, this)`
|
// `(super(..._args), <inits>, this)`
|
||||||
//
|
//
|
||||||
// TODO(improve-on-babel): When not in loose mode, inits are `_defineProperty(this, propName, value)`.
|
// TODO(improve-on-babel): When not in loose mode, inits are `_defineProperty(this, propName, value)`.
|
||||||
|
|
@ -357,7 +403,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
));
|
));
|
||||||
|
|
||||||
// Insert at top of function
|
// Insert at top of function
|
||||||
let body_stmts = Self::get_constructor_body_stmts(class, constructor_index);
|
let body_stmts = &mut constructor.body.as_mut().unwrap().statements;
|
||||||
body_stmts.insert(0, super_func_decl);
|
body_stmts.insert(0, super_func_decl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -420,17 +466,48 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
self.ctx.var_declarations.insert_let(super_binding, init, ctx);
|
self.ctx.var_declarations.insert_let(super_binding, init, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get body statements of constructor, given constructor's index within class elements.
|
/// Rename any symbols in constructor which clash with symbols used in initializers
|
||||||
fn get_constructor_body_stmts<'b>(
|
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 = ctx.ast.atom(&new_name);
|
||||||
|
// Rename symbol and binding
|
||||||
|
ctx.rename_symbol(symbol_id, constructor_scope_id, new_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get `Function` for constructor, given constructor's index within class elements.
|
||||||
|
fn get_constructor<'b>(
|
||||||
class: &'b mut Class<'a>,
|
class: &'b mut Class<'a>,
|
||||||
constructor_index: usize,
|
constructor_index: usize,
|
||||||
) -> &'b mut ArenaVec<'a, Statement<'a>> {
|
) -> &'b mut Function<'a> {
|
||||||
let constructor = match class.body.body.get_mut(constructor_index) {
|
let Some(ClassElement::MethodDefinition(method)) =
|
||||||
Some(ClassElement::MethodDefinition(constructor)) => constructor.as_mut(),
|
class.body.body.get_mut(constructor_index)
|
||||||
_ => unreachable!(),
|
else {
|
||||||
|
unreachable!()
|
||||||
};
|
};
|
||||||
debug_assert!(constructor.kind == MethodDefinitionKind::Constructor);
|
debug_assert!(method.kind == MethodDefinitionKind::Constructor);
|
||||||
&mut constructor.value.body.as_mut().unwrap().statements
|
&mut method.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,6 +524,11 @@ impl<'a, 'c> ConstructorParamsSuperReplacer<'a, 'c> {
|
||||||
Self { super_binding: None, ctx }
|
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(
|
fn replace(
|
||||||
mut self,
|
mut self,
|
||||||
constructor: &mut Function<'a>,
|
constructor: &mut Function<'a>,
|
||||||
|
|
@ -592,17 +674,24 @@ impl<'a, 'c> ConstructorBodySuperReplacer<'a, 'c> {
|
||||||
Self { constructor_scope_id, super_binding: None, ctx }
|
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(
|
fn replace(
|
||||||
mut self,
|
mut self,
|
||||||
body_stmts: &mut ArenaVec<'a, Statement<'a>>,
|
constructor: &mut Function<'a>,
|
||||||
) -> (ScopeId, InstanceInitsInsertLocation<'a>) {
|
) -> (ScopeId, InstanceInitsInsertLocation<'a>) {
|
||||||
|
let body_stmts = &mut constructor.body.as_mut().unwrap().statements;
|
||||||
let mut body_stmts_iter = body_stmts.iter_mut();
|
let mut body_stmts_iter = body_stmts.iter_mut();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut body_stmts_iter_enumerated = body_stmts_iter.by_ref().enumerate();
|
let mut body_stmts_iter_enumerated = body_stmts_iter.by_ref().enumerate();
|
||||||
if let Some((index, stmt)) = body_stmts_iter_enumerated.next() {
|
if let Some((index, stmt)) = body_stmts_iter_enumerated.next() {
|
||||||
// If statement is standalone `super()`, insert inits after `super()`.
|
// If statement is standalone `super()`, insert inits after `super()`.
|
||||||
// We can avoid a nested `_super` function for this common case.
|
// We can avoid a `_super` function for this common case.
|
||||||
if let Statement::ExpressionStatement(expr_stmt) = &*stmt {
|
if let Statement::ExpressionStatement(expr_stmt) = &*stmt {
|
||||||
if let Expression::CallExpression(call_expr) = &expr_stmt.expression {
|
if let Expression::CallExpression(call_expr) = &expr_stmt.expression {
|
||||||
if call_expr.callee.is_super() {
|
if call_expr.callee.is_super() {
|
||||||
|
|
@ -630,10 +719,11 @@ impl<'a, 'c> ConstructorBodySuperReplacer<'a, 'c> {
|
||||||
// could be. But this should very rarely happen in practice, and minifier will delete
|
// could be. But this should very rarely happen in practice, and minifier will delete
|
||||||
// the `_super` function as dead code.
|
// the `_super` function as dead code.
|
||||||
// TODO: Delete the initializers instead.
|
// TODO: Delete the initializers instead.
|
||||||
|
let super_func_scope_id = self.create_super_func_scope();
|
||||||
let super_binding = self.create_super_binding();
|
let super_binding = self.create_super_binding();
|
||||||
let insert_location =
|
let insert_location =
|
||||||
InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding);
|
InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding);
|
||||||
return (self.create_super_func_scope(), insert_location);
|
return (super_func_scope_id, insert_location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -737,6 +827,39 @@ impl<'a, 'c> ConstructorBodySuperReplacer<'a, 'c> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visitor to rename bindings and references.
|
||||||
|
struct ConstructorSymbolRenamer<'a, 'v> {
|
||||||
|
clashing_symbols: &'v mut FxHashMap<SymbolId, Atom<'a>>,
|
||||||
|
ctx: &'v TraverseCtx<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'v> ConstructorSymbolRenamer<'a, 'v> {
|
||||||
|
fn new(
|
||||||
|
clashing_symbols: &'v mut FxHashMap<SymbolId, Atom<'a>>,
|
||||||
|
ctx: &'v TraverseCtx<'a>,
|
||||||
|
) -> Self {
|
||||||
|
Self { clashing_symbols, ctx }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'v> VisitMut<'a> for ConstructorSymbolRenamer<'a, 'v> {
|
||||||
|
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.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) {
|
||||||
|
let reference_id = ident.reference_id();
|
||||||
|
if let Some(symbol_id) = self.ctx.symbols().get_reference(reference_id).symbol_id() {
|
||||||
|
if let Some(new_name) = self.clashing_symbols.get(&symbol_id) {
|
||||||
|
ident.name = new_name.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `super(...args);`
|
/// `super(...args);`
|
||||||
fn create_super_call<'a>(
|
fn create_super_call<'a>(
|
||||||
args_binding: &BoundIdentifier<'a>,
|
args_binding: &BoundIdentifier<'a>,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use oxc_ast::{ast::*, visit::Visit};
|
use oxc_ast::{ast::*, visit::Visit};
|
||||||
use oxc_syntax::scope::{ScopeFlags, ScopeId};
|
use oxc_span::Atom;
|
||||||
|
use oxc_syntax::{
|
||||||
|
scope::{ScopeFlags, ScopeId},
|
||||||
|
symbol::SymbolId,
|
||||||
|
};
|
||||||
use oxc_traverse::TraverseCtx;
|
use oxc_traverse::TraverseCtx;
|
||||||
|
|
||||||
use super::ClassProperties;
|
use super::ClassProperties;
|
||||||
|
|
@ -24,13 +30,24 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Visitor to change parent scope of first-level scopes in instance property initializer.
|
// TODO: If no `constructor_scope_id`, then don't need to traverse beyond first-level scope,
|
||||||
|
// as all we need to do is update scopes. Add a faster visitor for this more limited traversal.
|
||||||
|
|
||||||
|
/// 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> {
|
struct InstanceInitializerVisitor<'a, 'v> {
|
||||||
/// Incremented when entering a scope, decremented when exiting it.
|
/// Incremented when entering a scope, decremented when exiting it.
|
||||||
/// Parent `ScopeId` should be updated when `scope_depth == 0`.
|
/// Parent `ScopeId` should be updated when `scope_depth == 0`.
|
||||||
scope_depth: u32,
|
scope_depth: u32,
|
||||||
/// Parent scope
|
/// Parent scope
|
||||||
parent_scope_id: ScopeId,
|
parent_scope_id: ScopeId,
|
||||||
|
/// Constructor scope, if need to check for clashing bindings with constructor.
|
||||||
|
/// `None` if constructor is newly created, or inits are being inserted in `_super` function
|
||||||
|
/// outside class, because in those cases there are no bindings which can clash.
|
||||||
|
constructor_scope_id: Option<ScopeId>,
|
||||||
|
/// Clashing symbols
|
||||||
|
clashing_constructor_symbols: &'v mut FxHashMap<SymbolId, Atom<'a>>,
|
||||||
/// `TraverseCtx` object.
|
/// `TraverseCtx` object.
|
||||||
ctx: &'v mut TraverseCtx<'a>,
|
ctx: &'v mut TraverseCtx<'a>,
|
||||||
}
|
}
|
||||||
|
|
@ -40,24 +57,21 @@ impl<'a, 'v> InstanceInitializerVisitor<'a, 'v> {
|
||||||
class_properties: &'v mut ClassProperties<'a, '_>,
|
class_properties: &'v mut ClassProperties<'a, '_>,
|
||||||
ctx: &'v mut TraverseCtx<'a>,
|
ctx: &'v mut TraverseCtx<'a>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let parent_scope_id = class_properties.instance_inits_scope_id;
|
Self {
|
||||||
Self { scope_depth: 0, parent_scope_id, ctx }
|
scope_depth: 0,
|
||||||
|
parent_scope_id: class_properties.instance_inits_scope_id,
|
||||||
|
constructor_scope_id: class_properties.instance_inits_constructor_scope_id,
|
||||||
|
clashing_constructor_symbols: &mut class_properties.clashing_constructor_symbols,
|
||||||
|
ctx,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'v> Visit<'a> for InstanceInitializerVisitor<'a, 'v> {
|
impl<'a, 'v> Visit<'a> for InstanceInitializerVisitor<'a, 'v> {
|
||||||
/// Update parent scope for first level of scopes.
|
/// Update parent scope for first level of scopes.
|
||||||
/// Convert scope to sloppy mode if `self.make_sloppy_mode == true`.
|
|
||||||
fn enter_scope(&mut self, _flags: ScopeFlags, scope_id: &Cell<Option<ScopeId>>) {
|
fn enter_scope(&mut self, _flags: ScopeFlags, scope_id: &Cell<Option<ScopeId>>) {
|
||||||
let scope_id = scope_id.get().unwrap();
|
|
||||||
|
|
||||||
// TODO: Not necessary to do this check for all scopes.
|
|
||||||
// In JS, only `Function`, `ArrowFunctionExpression` or `Class` can be the first-level scope,
|
|
||||||
// as all other types which have a scope are statements or `StaticBlock` which would need to be
|
|
||||||
// inside a function or class. But some TS types with scopes could be first level via
|
|
||||||
// e.g. `TaggedTemplateExpression::type_parameters`, which contains `TSType`.
|
|
||||||
// Not sure if that matters though, as they'll be stripped out anyway by TS transform.
|
|
||||||
if self.scope_depth == 0 {
|
if self.scope_depth == 0 {
|
||||||
|
let scope_id = scope_id.get().unwrap();
|
||||||
self.reparent_scope(scope_id);
|
self.reparent_scope(scope_id);
|
||||||
}
|
}
|
||||||
self.scope_depth += 1;
|
self.scope_depth += 1;
|
||||||
|
|
@ -66,10 +80,27 @@ impl<'a, 'v> Visit<'a> for InstanceInitializerVisitor<'a, 'v> {
|
||||||
fn leave_scope(&mut self) {
|
fn leave_scope(&mut self) {
|
||||||
self.scope_depth -= 1;
|
self.scope_depth -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
|
||||||
|
let Some(constructor_scope_id) = self.constructor_scope_id else { return };
|
||||||
|
|
||||||
|
// 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(symbol_id) = self.ctx.scopes().get_binding(constructor_scope_id, &ident.name)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Optimization: Exit if reference is bound to symbol within initializer
|
||||||
|
|
||||||
|
self.clashing_constructor_symbols.entry(symbol_id).or_insert(ident.name.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'v> InstanceInitializerVisitor<'a, 'v> {
|
impl<'a, 'v> InstanceInitializerVisitor<'a, 'v> {
|
||||||
/// Update parent of scope to scope above class.
|
/// Update parent of scope.
|
||||||
fn reparent_scope(&mut self, scope_id: ScopeId) {
|
fn reparent_scope(&mut self, scope_id: ScopeId) {
|
||||||
self.ctx.scopes_mut().change_parent_id(scope_id, Some(self.parent_scope_id));
|
self.ctx.scopes_mut().change_parent_id(scope_id, Some(self.parent_scope_id));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,13 +145,14 @@
|
||||||
//! * Class properties TC39 proposal: <https://github.com/tc39/proposal-class-fields>
|
//! * Class properties TC39 proposal: <https://github.com/tc39/proposal-class-fields>
|
||||||
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use rustc_hash::FxBuildHasher;
|
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use oxc_allocator::{Address, GetAddress};
|
use oxc_allocator::{Address, GetAddress};
|
||||||
use oxc_ast::ast::*;
|
use oxc_ast::ast::*;
|
||||||
use oxc_data_structures::stack::NonEmptyStack;
|
use oxc_data_structures::stack::NonEmptyStack;
|
||||||
use oxc_syntax::scope::ScopeId;
|
use oxc_span::Atom;
|
||||||
|
use oxc_syntax::{scope::ScopeId, symbol::SymbolId};
|
||||||
use oxc_traverse::{Traverse, TraverseCtx};
|
use oxc_traverse::{Traverse, TraverseCtx};
|
||||||
|
|
||||||
use crate::TransformCtx;
|
use crate::TransformCtx;
|
||||||
|
|
@ -220,6 +221,12 @@ pub struct ClassProperties<'a, 'ctx> {
|
||||||
temp_var_is_created: bool,
|
temp_var_is_created: bool,
|
||||||
/// Scope that instance init initializers will be inserted into
|
/// Scope that instance init initializers will be inserted into
|
||||||
instance_inits_scope_id: ScopeId,
|
instance_inits_scope_id: ScopeId,
|
||||||
|
/// `ScopeId` of class constructor, if instance init initializers will be inserted into constructor.
|
||||||
|
/// Used for checking for variable name clashes.
|
||||||
|
/// e.g. `let x; class C { prop = x; constructor(x) {} }` - `x` in constructor needs to be renamed
|
||||||
|
instance_inits_constructor_scope_id: Option<ScopeId>,
|
||||||
|
/// `SymbolId`s in constructor which clash with instance prop initializers
|
||||||
|
clashing_constructor_symbols: FxHashMap<SymbolId, Atom<'a>>,
|
||||||
/// Expressions to insert before class
|
/// Expressions to insert before class
|
||||||
insert_before: Vec<Expression<'a>>,
|
insert_before: Vec<Expression<'a>>,
|
||||||
/// Expressions to insert after class expression
|
/// Expressions to insert after class expression
|
||||||
|
|
@ -252,7 +259,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
|
||||||
class_bindings: ClassBindings::default(),
|
class_bindings: ClassBindings::default(),
|
||||||
temp_var_is_created: false,
|
temp_var_is_created: false,
|
||||||
instance_inits_scope_id: ScopeId::new(0),
|
instance_inits_scope_id: ScopeId::new(0),
|
||||||
|
instance_inits_constructor_scope_id: None,
|
||||||
// `Vec`s and `FxHashMap`s which are reused for every class being transformed
|
// `Vec`s and `FxHashMap`s which are reused for every class being transformed
|
||||||
|
clashing_constructor_symbols: FxHashMap::default(),
|
||||||
insert_before: vec![],
|
insert_before: vec![],
|
||||||
insert_after_exprs: vec![],
|
insert_after_exprs: vec![],
|
||||||
insert_after_stmts: vec![],
|
insert_after_stmts: vec![],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
commit: 54a8389f
|
commit: 54a8389f
|
||||||
|
|
||||||
Passed: 582/927
|
Passed: 593/927
|
||||||
|
|
||||||
# All Passed:
|
# All Passed:
|
||||||
* babel-plugin-transform-class-static-block
|
* babel-plugin-transform-class-static-block
|
||||||
|
|
@ -276,7 +276,7 @@ x Output mismatch
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
|
|
||||||
# babel-plugin-transform-class-properties (194/264)
|
# babel-plugin-transform-class-properties (205/264)
|
||||||
* assumption-constantSuper/complex-super-class/input.js
|
* assumption-constantSuper/complex-super-class/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
|
|
@ -295,18 +295,12 @@ x Output mismatch
|
||||||
* assumption-setPublicClassFields/computed/input.js
|
* assumption-setPublicClassFields/computed/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* assumption-setPublicClassFields/constructor-collision/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* assumption-setPublicClassFields/static-infer-name/input.js
|
* assumption-setPublicClassFields/static-infer-name/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* assumption-setPublicClassFields/static-super-loose/input.js
|
* assumption-setPublicClassFields/static-super-loose/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* assumption-setPublicClassFields/super-with-collision/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* class-name-tdz/general/input.js
|
* class-name-tdz/general/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
|
|
@ -341,12 +335,6 @@ x Output mismatch
|
||||||
* private/class-shadow-builtins/input.mjs
|
* private/class-shadow-builtins/input.mjs
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* private/constructor-collision/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* private/extracted-this/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* private/nested-class-computed-redeclared/input.js
|
* private/nested-class-computed-redeclared/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
|
|
@ -386,12 +374,6 @@ x Output mismatch
|
||||||
* private-loose/class-shadow-builtins/input.mjs
|
* private-loose/class-shadow-builtins/input.mjs
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* private-loose/constructor-collision/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* private-loose/extracted-this/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* private-loose/nested-class-computed-redeclared/input.js
|
* private-loose/nested-class-computed-redeclared/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
|
|
@ -463,39 +445,24 @@ x Output mismatch
|
||||||
* public/computed/input.js
|
* public/computed/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* public/constructor-collision/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* public/delete-super-property/input.js
|
* public/delete-super-property/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* public/extracted-this/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* public/static-infer-name/input.js
|
* public/static-infer-name/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* public/super-with-collision/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* public-loose/class-shadow-builtins/input.mjs
|
* public-loose/class-shadow-builtins/input.mjs
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* public-loose/computed/input.js
|
* public-loose/computed/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* public-loose/constructor-collision/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* public-loose/static-infer-name/input.js
|
* public-loose/static-infer-name/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* public-loose/static-super/input.js
|
* public-loose/static-super/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
* public-loose/super-with-collision/input.js
|
|
||||||
x Output mismatch
|
|
||||||
|
|
||||||
* regression/6153/input.js
|
* regression/6153/input.js
|
||||||
x Output mismatch
|
x Output mismatch
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ commit: 54a8389f
|
||||||
|
|
||||||
node: v22.12.0
|
node: v22.12.0
|
||||||
|
|
||||||
Passed: 189 of 215 (87.91%)
|
Passed: 191 of 215 (88.84%)
|
||||||
|
|
||||||
Failures:
|
Failures:
|
||||||
|
|
||||||
|
|
@ -24,14 +24,6 @@ Unexpected token `[`. Expected * for generator, private key, identifier or async
|
||||||
AssertionError: expected undefined to be 'hello' // Object.is equality
|
AssertionError: expected undefined to be 'hello' // Object.is equality
|
||||||
at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-nested-class-super-property-in-decorator-exec.test.js:22:28
|
at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-nested-class-super-property-in-decorator-exec.test.js:22:28
|
||||||
|
|
||||||
./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-constructor-collision-exec.test.js
|
|
||||||
AssertionError: expected undefined to be 'bar' // Object.is equality
|
|
||||||
at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-constructor-collision-exec.test.js:18:19
|
|
||||||
|
|
||||||
./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-constructor-collision-exec.test.js
|
|
||||||
AssertionError: expected undefined to be 'bar' // Object.is equality
|
|
||||||
at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-constructor-collision-exec.test.js:21:19
|
|
||||||
|
|
||||||
./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-nested-class-computed-redeclared-exec.test.js
|
./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-nested-class-computed-redeclared-exec.test.js
|
||||||
Private field '#foo' must be declared in an enclosing class
|
Private field '#foo' must be declared in an enclosing class
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue