diff --git a/crates/oxc_transformer/src/es2022/class_properties/class.rs b/crates/oxc_transformer/src/es2022/class_properties/class.rs index 1904964503a153..1f33396569fb6b 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/class.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/class.rs @@ -409,14 +409,18 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { None } else if let Some(constructor) = constructor { // Existing constructor + // TODO: Set `self.instance_inits_constructor_scope_id = None` if constructor has no bindings let constructor = constructor.value.as_mut(); 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.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) } else { - self.instance_inits_scope_id = constructor.scope_id(); + let constructor_scope_id = constructor.scope_id(); + self.instance_inits_scope_id = constructor_scope_id; + self.instance_inits_constructor_scope_id = Some(constructor_scope_id); Some(InstanceInitsInsertLocation::ExistingConstructor(0)) } } else { @@ -427,6 +431,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { ScopeFlags::Function | ScopeFlags::Constructor | ScopeFlags::StrictMode, ); self.instance_inits_scope_id = constructor_scope_id; + self.instance_inits_constructor_scope_id = None; Some(InstanceInitsInsertLocation::NewConstructor) }; diff --git a/crates/oxc_transformer/src/es2022/class_properties/constructor.rs b/crates/oxc_transformer/src/es2022/class_properties/constructor.rs index ae497c34edfa47..03e832aec0542e 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/constructor.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/constructor.rs @@ -99,13 +99,15 @@ //! ESBuild does not handle `super()` in constructor params correctly: //! [ESBuild REPL](https://esbuild.github.io/try/#dAAwLjI0LjAALS10YXJnZXQ9ZXMyMDIwAGNsYXNzIEMgZXh0ZW5kcyBTIHsKICBwcm9wID0gZm9vKCk7CiAgY29uc3RydWN0b3IoeCA9IHN1cGVyKCksIHkgPSBzdXBlcigpKSB7fQp9Cg) +use rustc_hash::FxHashMap; + use oxc_allocator::Vec as ArenaVec; use oxc_ast::{ast::*, visit::walk_mut, VisitMut, NONE}; use oxc_span::SPAN; use oxc_syntax::{ node::NodeId, scope::{ScopeFlags, ScopeId}, - symbol::SymbolFlags, + symbol::{SymbolFlags, SymbolId}, }; use oxc_traverse::{BoundIdentifier, TraverseCtx}; @@ -126,11 +128,21 @@ pub(super) enum InstanceInitsInsertLocation<'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) + pub constructor_scope_id: Option, +} + impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// Replace `super()` call(s) in constructor, if required. /// - /// Returns `InstanceInitsInsertLocation` detailing where instance property initializers - /// should be inserted. + /// Returns: + /// * `InstanceInitScopes` details the `ScopeId`s required for transforming instance property initializers. + /// * `InstanceInitsInsertLocation` detailing where instance property initializers should be inserted. /// /// * `super()` first appears as a top level statement in constructor body (common case): /// * Do not alter constructor. @@ -151,14 +163,15 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// /// 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 - /// are recorded in the returned `InstanceInitsInsertLocation`. + /// If a `_super` function is required, binding for `_super` is recorded in the returned + /// `InstanceInitsInsertLocation`, and `ScopeId` from `_super` function is returned as + /// `insert_in_scope_id` in returned `InstanceInitScopes`. /// /// This function does not create the `_super` function or insert it. That happens later. pub(super) fn replace_super_in_constructor( constructor: &mut Function<'a>, ctx: &mut TraverseCtx<'a>, - ) -> (ScopeId, InstanceInitsInsertLocation<'a>) { + ) -> (InstanceInitScopes, InstanceInitsInsertLocation<'a>) { // Find any `super()`s in constructor params and replace with `_super.call(super())` let replacer = ConstructorParamsSuperReplacer::new(ctx); if let Some(result) = replacer.replace(constructor) { @@ -172,7 +185,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { replacer.replace(body_stmts) } - /// Insert property initializers into existing class constructor. + /// Insert instance property initializers. /// /// `scope_id` has different meaning depending on type of `insertion_location`. pub(super) fn insert_instance_inits( @@ -193,7 +206,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { Self::insert_constructor(class, scope_id, inits, ctx); } InstanceInitsInsertLocation::ExistingConstructor(stmt_index) => { - Self::insert_inits_into_constructor_as_statements( + self.insert_inits_into_constructor_as_statements( class, inits, constructor_index, @@ -202,7 +215,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { ); } InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding) => { - Self::create_super_function_inside_constructor( + self.create_super_function_inside_constructor( class, inits, super_binding, @@ -282,13 +295,19 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// Insert instance property initializers into constructor body at `insertion_index`. fn insert_inits_into_constructor_as_statements( + &mut self, class: &mut Class<'a>, inits: Vec>, constructor_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)); } @@ -296,6 +315,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// and insert at top of constructor body. /// `var _super = (..._args) => (super(..._args), , this);` fn create_super_function_inside_constructor( + &mut self, class: &mut Class<'a>, inits: Vec>, super_binding: &BoundIdentifier<'a>, @@ -303,6 +323,10 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { constructor_index: usize, 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), , this)` // // TODO(improve-on-babel): When not in loose mode, inits are `_defineProperty(this, propName, value)`. @@ -357,7 +381,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { )); // 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); } @@ -420,17 +444,47 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { self.ctx.var_declarations.insert_let(super_binding, init, ctx); } + /// Rename any symbols in constructor which clash with symbols used in initializers + fn rename_clashing_symbols( + &mut self, + constructor: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.clashing_constructor_symbols.is_empty() { + return; + } + + // Rename symbols to UIDs + let constructor_scope_id = constructor.scope_id(); + for (&symbol_id, name) in &mut self.clashing_constructor_symbols { + // Generate replacement UID name + let new_name = ctx.generate_uid_name(name); + // Save replacement name in `clashing_constructor_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(&mut self.clashing_constructor_symbols, ctx); + renamer.visit_function(constructor, ScopeFlags::empty()); + + // Empty `clashing_constructor_symbols` hashmap for reuse on next class + self.clashing_constructor_symbols.clear(); + } + /// Get body statements of constructor, given constructor's index within class elements. - fn get_constructor_body_stmts<'b>( + fn get_constructor<'b>( class: &'b mut Class<'a>, constructor_index: usize, - ) -> &'b mut ArenaVec<'a, Statement<'a>> { + ) -> &'b mut Function<'a> { let constructor = match class.body.body.get_mut(constructor_index) { Some(ClassElement::MethodDefinition(constructor)) => constructor.as_mut(), _ => unreachable!(), }; debug_assert!(constructor.kind == MethodDefinitionKind::Constructor); - &mut constructor.value.body.as_mut().unwrap().statements + &mut constructor.value } } @@ -450,7 +504,7 @@ impl<'a, 'c> ConstructorParamsSuperReplacer<'a, 'c> { fn replace( mut self, constructor: &mut Function<'a>, - ) -> Option<(ScopeId, InstanceInitsInsertLocation<'a>)> { + ) -> Option<(InstanceInitScopes, InstanceInitsInsertLocation<'a>)> { self.visit_formal_parameters(&mut constructor.params); #[expect(clippy::question_mark)] @@ -479,8 +533,12 @@ impl<'a, 'c> ConstructorParamsSuperReplacer<'a, 'c> { NodeId::DUMMY, ScopeFlags::Function | ScopeFlags::StrictMode, ); + let insert_scopes = InstanceInitScopes { + insert_in_scope_id: super_func_scope_id, + constructor_scope_id: None, + }; - Some((super_func_scope_id, insert_location)) + Some((insert_scopes, insert_location)) } } @@ -595,20 +653,24 @@ impl<'a, 'c> ConstructorBodySuperReplacer<'a, 'c> { fn replace( mut self, body_stmts: &mut ArenaVec<'a, Statement<'a>>, - ) -> (ScopeId, InstanceInitsInsertLocation<'a>) { + ) -> (InstanceInitScopes, InstanceInitsInsertLocation<'a>) { let mut body_stmts_iter = body_stmts.iter_mut(); loop { let mut body_stmts_iter_enumerated = body_stmts_iter.by_ref().enumerate(); if let Some((index, stmt)) = body_stmts_iter_enumerated.next() { // 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 Expression::CallExpression(call_expr) = &expr_stmt.expression { if call_expr.callee.is_super() { let insert_location = InstanceInitsInsertLocation::ExistingConstructor(index + 1); - return (self.constructor_scope_id, insert_location); + let insert_scopes = InstanceInitScopes { + insert_in_scope_id: self.constructor_scope_id, + constructor_scope_id: Some(self.constructor_scope_id), + }; + return (insert_scopes, insert_location); } } } @@ -630,10 +692,14 @@ impl<'a, 'c> ConstructorBodySuperReplacer<'a, 'c> { // could be. But this should very rarely happen in practice, and minifier will delete // the `_super` function as dead code. // TODO: Delete the initializers instead. + let insert_scopes = InstanceInitScopes { + insert_in_scope_id: self.create_super_func_scope(), + constructor_scope_id: Some(self.constructor_scope_id), + }; let super_binding = self.create_super_binding(); let insert_location = InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding); - return (self.create_super_func_scope(), insert_location); + return (insert_scopes, insert_location); } } @@ -643,10 +709,13 @@ impl<'a, 'c> ConstructorBodySuperReplacer<'a, 'c> { self.visit_statement(stmt); } - let super_func_scope_id = self.create_super_func_scope(); + let insert_scopes = InstanceInitScopes { + insert_in_scope_id: self.create_super_func_scope(), + constructor_scope_id: Some(self.constructor_scope_id), + }; let super_binding = self.super_binding.unwrap(); let insert_location = InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding); - (super_func_scope_id, insert_location) + (insert_scopes, insert_location) } /// Create scope for `_super` function inside constructor body @@ -737,6 +806,39 @@ impl<'a, 'c> ConstructorBodySuperReplacer<'a, 'c> { } } +/// Visitor to rename bindings and references. +struct ConstructorSymbolRenamer<'a, 'v> { + clashing_symbols: &'v mut FxHashMap>, + ctx: &'v TraverseCtx<'a>, +} + +impl<'a, 'v> ConstructorSymbolRenamer<'a, 'v> { + fn new( + clashing_symbols: &'v mut FxHashMap>, + ctx: &'v TraverseCtx<'a>, + ) -> Self { + Self { clashing_symbols, ctx } + } +} + +impl<'a, '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);` fn create_super_call<'a>( args_binding: &BoundIdentifier<'a>, diff --git a/crates/oxc_transformer/src/es2022/class_properties/instance_prop_init.rs b/crates/oxc_transformer/src/es2022/class_properties/instance_prop_init.rs index 0f4b25595f1268..d077871ed5b6a6 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/instance_prop_init.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/instance_prop_init.rs @@ -3,8 +3,14 @@ use std::cell::Cell; +use rustc_hash::FxHashMap; + 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 super::ClassProperties; @@ -24,6 +30,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { } } +// 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. struct InstanceInitializerVisitor<'a, 'v> { /// Incremented when entering a scope, decremented when exiting it. @@ -31,6 +40,12 @@ struct InstanceInitializerVisitor<'a, 'v> { scope_depth: u32, /// Parent scope 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, + /// Clashing symbols + clashing_constructor_symbols: &'v mut FxHashMap>, /// `TraverseCtx` object. ctx: &'v mut TraverseCtx<'a>, } @@ -40,8 +55,13 @@ impl<'a, 'v> InstanceInitializerVisitor<'a, 'v> { class_properties: &'v mut ClassProperties<'a, '_>, ctx: &'v mut TraverseCtx<'a>, ) -> Self { - let parent_scope_id = class_properties.instance_inits_scope_id; - Self { scope_depth: 0, parent_scope_id, ctx } + Self { + 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, + } } } @@ -66,6 +86,23 @@ impl<'a, 'v> Visit<'a> for InstanceInitializerVisitor<'a, 'v> { fn leave_scope(&mut self) { 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: 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> { diff --git a/crates/oxc_transformer/src/es2022/class_properties/mod.rs b/crates/oxc_transformer/src/es2022/class_properties/mod.rs index ca2e661afded92..eab8806afdb593 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/mod.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/mod.rs @@ -145,13 +145,14 @@ //! * Class properties TC39 proposal: use indexmap::IndexMap; -use rustc_hash::FxBuildHasher; +use rustc_hash::{FxBuildHasher, FxHashMap}; use serde::Deserialize; use oxc_allocator::{Address, GetAddress}; use oxc_ast::ast::*; 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 crate::TransformCtx; @@ -220,6 +221,12 @@ pub struct ClassProperties<'a, 'ctx> { temp_var_is_created: bool, /// Scope that instance init initializers will be inserted into 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, + /// `SymbolId`s in constructor which clash with instance prop initializers + clashing_constructor_symbols: FxHashMap>, /// Expressions to insert before class insert_before: Vec>, /// Expressions to insert after class expression @@ -252,7 +259,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { class_bindings: ClassBindings::default(), temp_var_is_created: false, instance_inits_scope_id: ScopeId::new(0), - // `Vec`s and `FxHashMap`s which are reused for every class being transformed + instance_inits_constructor_scope_id: None, + // `Vec`s and `FxHashSet`s which are reused for every class being transformed + clashing_constructor_symbols: FxHashMap::default(), insert_before: vec![], insert_after_exprs: vec![], insert_after_stmts: vec![], diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index 6fe4b22662e976..3d1033c40d1d59 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,6 +1,6 @@ commit: 54a8389f -Passed: 582/927 +Passed: 593/927 # All Passed: * babel-plugin-transform-class-static-block @@ -276,7 +276,7 @@ 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 x Output mismatch @@ -295,18 +295,12 @@ x Output mismatch * assumption-setPublicClassFields/computed/input.js x Output mismatch -* assumption-setPublicClassFields/constructor-collision/input.js -x Output mismatch - * assumption-setPublicClassFields/static-infer-name/input.js x Output mismatch * assumption-setPublicClassFields/static-super-loose/input.js x Output mismatch -* assumption-setPublicClassFields/super-with-collision/input.js -x Output mismatch - * class-name-tdz/general/input.js x Output mismatch @@ -341,12 +335,6 @@ x Output mismatch * private/class-shadow-builtins/input.mjs 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 x Output mismatch @@ -386,12 +374,6 @@ x Output mismatch * private-loose/class-shadow-builtins/input.mjs 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 x Output mismatch @@ -463,39 +445,24 @@ x Output mismatch * public/computed/input.js x Output mismatch -* public/constructor-collision/input.js -x Output mismatch - * public/delete-super-property/input.js x Output mismatch -* public/extracted-this/input.js -x Output mismatch - * public/static-infer-name/input.js x Output mismatch -* public/super-with-collision/input.js -x Output mismatch - * public-loose/class-shadow-builtins/input.mjs x Output mismatch * public-loose/computed/input.js x Output mismatch -* public-loose/constructor-collision/input.js -x Output mismatch - * public-loose/static-infer-name/input.js x Output mismatch * public-loose/static-super/input.js x Output mismatch -* public-loose/super-with-collision/input.js -x Output mismatch - * regression/6153/input.js x Output mismatch diff --git a/tasks/transform_conformance/snapshots/babel_exec.snap.md b/tasks/transform_conformance/snapshots/babel_exec.snap.md index 42d1055fe368f9..afa017ab6f152f 100644 --- a/tasks/transform_conformance/snapshots/babel_exec.snap.md +++ b/tasks/transform_conformance/snapshots/babel_exec.snap.md @@ -2,7 +2,7 @@ commit: 54a8389f node: v22.12.0 -Passed: 189 of 215 (87.91%) +Passed: 191 of 215 (88.84%) Failures: @@ -24,14 +24,6 @@ Unexpected token `[`. Expected * for generator, private key, identifier or async 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 -./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 Private field '#foo' must be declared in an enclosing class