diff --git a/crates/oxc_transformer/src/es2022/class_properties/static_prop_init.rs b/crates/oxc_transformer/src/es2022/class_properties/static_prop_init.rs index fa8b0f7578a3e..86ecf6e3f3315 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/static_prop_init.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/static_prop_init.rs @@ -1,11 +1,13 @@ //! ES2022: Class Properties //! Transform of static property initializers. +use std::cell::Cell; + use oxc_ast::{ ast::*, visit::{walk_mut, VisitMut}, }; -use oxc_syntax::scope::ScopeFlags; +use oxc_syntax::scope::{ScopeFlags, ScopeId}; use oxc_traverse::{BoundIdentifier, TraverseCtx}; use super::ClassProperties; @@ -68,9 +70,11 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// * Class expression: `x = class C { static #x = 123; static y = this.#x; }` /// -> `var _C, _x; x = (_C = class C {}, _x = { _: 123 }, _C.y = _assertClassBrand(_C, _C, _x)._), _C)` /// -/// Reason we need to do this is because the initializer is being moved from inside the class to outside. -/// `this` outside the class refers to a different `this`, and private fields are only valid within the -/// class body. So we need to transform them. +/// Also sets `ScopeFlags` of scopes to sloppy mode if code outside the class is sloppy mode. +/// +/// Reason we need to transform `this` is because the initializer is being moved from inside the class +/// to outside. `this` outside the class refers to a different `this`, and private fields are only valid +/// within the class body. So we need to transform them. /// /// Note that for class declarations, assignments are made to properties of original class name `C`, /// but temp var `_C` is used in replacements for `this` or class name, and private fields. @@ -88,8 +92,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// assert(C2.getSelf2() === C); // Would fail if `C` in `getSelf2` was not replaced with temp var /// ``` /// -/// If this class defines no private properties and class has no name, we only need to transform `this`, -/// so can skip traversing into functions and other contexts which have their own `this`. +/// If this class defines no private properties, class has no name, and no `ScopeFlags` need updating, +/// then we only need to transform `this`. So can skip traversing into functions and other contexts +/// which have their own `this`. /// /// Note: Those functions could contain private fields referring to a *parent* class's private props, /// but we don't need to transform them here as they remain in same class scope. @@ -99,12 +104,18 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { // 2. `this` / reference to class name / private field is not in a nested function, so we know the // code runs immediately, before any mutation of the class name binding can occur. // +// TODO(improve-on-babel): Updating `ScopeFlags` for strict mode makes semantic correctly for the output, +// but actually the transform isn't right. Should wrap initializer in a strict mode IIFE so that +// initializer code runs in strict mode, as it was before within class body. +// // TODO: Also re-parent child scopes. -// TODO: Alter scope flags on all scopes to remove `StrictMode`, if outside class is sloppy mode. -// Or fix this properly by wrapping all static prop initializers in a strict mode arrow function IIFE. struct StaticInitializerVisitor<'a, 'ctx, 'v> { - /// `true` if class has name or private properties. - class_has_name_or_private_props: bool, + /// `true` if class has name, or class has private properties, or `ScopeFlags` need updating. + /// Any of these neccesitates walking the whole tree. If none of those apply, we only need to + /// walk as far as functions and other constructs which define a `this`. + walk_deep: bool, + /// `true` if should make scopes sloppy mode + make_sloppy_mode: bool, /// Incremented when entering a different `this` context, decremented when exiting it. /// `this` should be transformed when `this_depth == 0`. this_depth: u32, @@ -119,9 +130,12 @@ impl<'a, 'ctx, 'v> StaticInitializerVisitor<'a, 'ctx, 'v> { class_properties: &'v mut ClassProperties<'a, 'ctx>, ctx: &'v mut TraverseCtx<'a>, ) -> Self { + let make_sloppy_mode = !ctx.current_scope_flags().is_strict_mode(); Self { - class_has_name_or_private_props: class_properties.class_bindings.name.is_some() + walk_deep: make_sloppy_mode + || class_properties.class_bindings.name.is_some() || class_properties.private_props_stack.last().is_some(), + make_sloppy_mode, this_depth: 0, class_properties, ctx, @@ -224,24 +238,67 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { self.replace_class_name_with_temp_var(ident); } + /// Convert scope to sloppy mode if `self.make_sloppy_mode == true` + fn enter_scope(&mut self, _flags: ScopeFlags, scope_id: &Cell>) { + if self.make_sloppy_mode { + *self.ctx.scopes_mut().get_flags_mut(scope_id.get().unwrap()) -= ScopeFlags::StrictMode; + } + } + // Increment `this_depth` when entering code where `this` refers to a different `this` // from `this` within this class, and decrement it when exiting. // Therefore `this_depth == 0` when `this` refers to the `this` which needs to be transformed. // - // Or, if class has no private properties, stop traversing entirely. No private field accesses - // need to be transformed, so no point searching for them. + // Or, if class has no name, class has no private properties, and `ScopeFlags` don't need updating, + // stop traversing entirely. No private field accesses need to be transformed, and no scopes need + // flags updating, so no point searching for them. + // + // Also set `make_sloppy_mode = false` while traversing a construct which is strict mode. + #[inline] fn visit_function(&mut self, func: &mut Function<'a>, flags: ScopeFlags) { - if self.class_has_name_or_private_props { + let parent_sloppy_mode = self.make_sloppy_mode; + if self.make_sloppy_mode && func.has_use_strict_directive() { + // Function has a `"use strict"` directive in body + self.make_sloppy_mode = false; + } + + if self.walk_deep { self.this_depth += 1; walk_mut::walk_function(self, func, flags); self.this_depth -= 1; } + + self.make_sloppy_mode = parent_sloppy_mode; + } + + #[inline] + fn visit_arrow_function_expression(&mut self, func: &mut ArrowFunctionExpression<'a>) { + let parent_sloppy_mode = self.make_sloppy_mode; + if self.make_sloppy_mode && func.has_use_strict_directive() { + // Arrow function has a `"use strict"` directive in body + self.make_sloppy_mode = false; + } + + walk_mut::walk_arrow_function_expression(self, func); + + self.make_sloppy_mode = parent_sloppy_mode; + } + + #[inline] + fn visit_class(&mut self, class: &mut Class<'a>) { + let parent_sloppy_mode = self.make_sloppy_mode; + // Classes are always strict mode + self.make_sloppy_mode = false; + + walk_mut::walk_class(self, class); + + self.make_sloppy_mode = parent_sloppy_mode; } #[inline] fn visit_static_block(&mut self, block: &mut StaticBlock<'a>) { - if self.class_has_name_or_private_props { + if self.walk_deep { self.this_depth += 1; walk_mut::walk_static_block(self, block); self.this_depth -= 1; @@ -250,11 +307,19 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { #[inline] fn visit_ts_module_block(&mut self, block: &mut TSModuleBlock<'a>) { - if self.class_has_name_or_private_props { + let parent_sloppy_mode = self.make_sloppy_mode; + if self.make_sloppy_mode && block.has_use_strict_directive() { + // Block has a `"use strict"` directive in body + self.make_sloppy_mode = false; + } + + if self.walk_deep { self.this_depth += 1; walk_mut::walk_ts_module_block(self, block); self.this_depth -= 1; } + + self.make_sloppy_mode = parent_sloppy_mode; } #[inline] @@ -274,7 +339,7 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { self.visit_property_key(&mut prop.key); } - if self.class_has_name_or_private_props { + if self.walk_deep { if let Some(value) = &mut prop.value { self.this_depth += 1; self.visit_expression(value); @@ -292,7 +357,7 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { self.visit_property_key(&mut prop.key); } - if self.class_has_name_or_private_props { + if self.walk_deep { if let Some(value) = &mut prop.value { self.this_depth += 1; self.visit_expression(value); diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index 2852eac125101..037ef32cab428 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -345,9 +345,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -368,9 +365,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -456,9 +450,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(6)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3), ScopeId(4)] rebuilt : ScopeId(1): [ScopeId(2), ScopeId(3)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(6): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(6): Some(ScopeId(0)) @@ -470,9 +461,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(6)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3), ScopeId(4)] rebuilt : ScopeId(1): [ScopeId(2), ScopeId(3)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(6): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(6): Some(ScopeId(0)) @@ -496,9 +484,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(6)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3), ScopeId(4)] rebuilt : ScopeId(1): [ScopeId(2), ScopeId(3)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(6): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(6): Some(ScopeId(0)) @@ -516,9 +501,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(6)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3), ScopeId(4)] rebuilt : ScopeId(1): [ScopeId(2), ScopeId(3)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(6): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(6): Some(ScopeId(0)) @@ -530,9 +512,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(6)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3), ScopeId(4)] rebuilt : ScopeId(1): [ScopeId(2), ScopeId(3)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(6): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(6): Some(ScopeId(0)) @@ -544,9 +523,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(5)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3), ScopeId(4)] rebuilt : ScopeId(1): [ScopeId(2), ScopeId(3)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(5): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(5): Some(ScopeId(0)) @@ -590,9 +566,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(3)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3)] rebuilt : ScopeId(1): [ScopeId(2)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(3): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(3): Some(ScopeId(0)) @@ -604,9 +577,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -624,9 +594,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -734,9 +701,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(3)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3)] rebuilt : ScopeId(1): [ScopeId(2)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(3): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(3): Some(ScopeId(0)) @@ -748,9 +712,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -765,9 +726,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -845,9 +803,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -865,9 +820,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -931,9 +883,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) @@ -951,9 +900,6 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2)] rebuilt : ScopeId(1): [] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) -rebuilt : ScopeId(2): ScopeFlags(Function | Arrow) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(2): Some(ScopeId(0)) diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index dcb998a11fd8c..bd67702933ae2 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: 54a8389f -Passed: 103/116 +Passed: 103/117 # All Passed: * babel-plugin-transform-class-static-block @@ -16,7 +16,7 @@ Passed: 103/116 * regexp -# babel-plugin-transform-class-properties (4/6) +# babel-plugin-transform-class-properties (4/7) * private-loose-tagged-template/input.js Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3), ScopeId(4)] @@ -35,13 +35,33 @@ rebuilt : ScopeId(0): [ScopeId(1), ScopeId(3)] Scope children mismatch: after transform: ScopeId(1): [ScopeId(2), ScopeId(3)] rebuilt : ScopeId(1): [ScopeId(2)] -Scope flags mismatch: -after transform: ScopeId(2): ScopeFlags(StrictMode | Function) -rebuilt : ScopeId(3): ScopeFlags(Function) Scope parent mismatch: after transform: ScopeId(2): Some(ScopeId(1)) rebuilt : ScopeId(3): Some(ScopeId(0)) +* static-prop-initializer-strict-mode/input.js +Scope children mismatch: +after transform: ScopeId(0): [ScopeId(1)] +rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2), ScopeId(8), ScopeId(14), ScopeId(17), ScopeId(20)] +Scope children mismatch: +after transform: ScopeId(1): [ScopeId(2), ScopeId(8), ScopeId(14), ScopeId(17), ScopeId(20)] +rebuilt : ScopeId(1): [] +Scope parent mismatch: +after transform: ScopeId(2): Some(ScopeId(1)) +rebuilt : ScopeId(2): Some(ScopeId(0)) +Scope parent mismatch: +after transform: ScopeId(8): Some(ScopeId(1)) +rebuilt : ScopeId(8): Some(ScopeId(0)) +Scope parent mismatch: +after transform: ScopeId(14): Some(ScopeId(1)) +rebuilt : ScopeId(14): Some(ScopeId(0)) +Scope parent mismatch: +after transform: ScopeId(17): Some(ScopeId(1)) +rebuilt : ScopeId(17): Some(ScopeId(0)) +Scope parent mismatch: +after transform: ScopeId(20): Some(ScopeId(1)) +rebuilt : ScopeId(20): Some(ScopeId(0)) + # babel-plugin-transform-async-to-generator (14/15) * super/nested/input.js diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/static-prop-initializer-strict-mode/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/static-prop-initializer-strict-mode/input.js new file mode 100644 index 0000000000000..67261f44d1ef5 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/static-prop-initializer-strict-mode/input.js @@ -0,0 +1,47 @@ +// Just to make sure we're in sloppy mode. This is a syntax error in strict mode. +delete x; + +class C { + static arrow = () => { + if (true) { + if (true) { + { + let f = function foo() {}; + } + } + } + return () => {}; + }; + + static fn = function() { + if (true) { + if (true) { + { + let f = function foo() {} + } + } + } + return () => {}; + }; + + static arrowStrict = () => { + "use strict"; + if (true) {} + return () => {}; + }; + + static fnStrict = function() { + "use strict"; + if (true) {} + return () => {}; + }; + + static klass = class extends function() {} { + constructor() {} + method() { + if (true) {} + function foo() {} + } + [() => {}]() {} + }; +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/static-prop-initializer-strict-mode/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/static-prop-initializer-strict-mode/output.js new file mode 100644 index 0000000000000..f57c167eb780f --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/static-prop-initializer-strict-mode/output.js @@ -0,0 +1,47 @@ +// Just to make sure we're in sloppy mode. This is a syntax error in strict mode. +delete x; + +class C {} + +babelHelpers.defineProperty(C, "arrow", () => { + if (true) { + if (true) { + { + let f = function foo() {} + } + } + } + return () => {}; +}); + +babelHelpers.defineProperty(C, "fn", function() { + if (true) { + if (true) { + { + let f = function foo() {} + } + } + } + return () => {}; +}); + +babelHelpers.defineProperty(C, "arrowStrict", () => { + "use strict"; + if (true) {} + return () => {}; +}); + +babelHelpers.defineProperty(C, "fnStrict", function() { + "use strict"; + if (true) {} + return () => {}; +}); + +babelHelpers.defineProperty(C, "klass", class extends function() {} { + constructor() {} + method() { + if (true) {} + function foo() {} + } + [() => {}]() {} +});