diff --git a/crates/oxc_transformer/src/common/helper_loader.rs b/crates/oxc_transformer/src/common/helper_loader.rs index 60cc96d5b8303..90452db527ff1 100644 --- a/crates/oxc_transformer/src/common/helper_loader.rs +++ b/crates/oxc_transformer/src/common/helper_loader.rs @@ -163,6 +163,7 @@ pub enum Helper { SuperPropSet, ReadOnlyError, WriteOnlyError, + CheckInRHS, } impl Helper { @@ -191,6 +192,7 @@ impl Helper { Self::SuperPropSet => "superPropSet", Self::ReadOnlyError => "readOnlyError", Self::WriteOnlyError => "writeOnlyError", + Self::CheckInRHS => "checkInRHS", } } } diff --git a/crates/oxc_transformer/src/es2022/class_properties/mod.rs b/crates/oxc_transformer/src/es2022/class_properties/mod.rs index 8cf5f47a797a6..9e6e1092d91cf 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/mod.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/mod.rs @@ -368,6 +368,10 @@ impl<'a, 'ctx> Traverse<'a> for ClassProperties<'a, 'ctx> { Expression::TaggedTemplateExpression(_) => { self.transform_tagged_template_expression(expr, ctx); } + // "#prop in object" + Expression::PrivateInExpression(_) => { + self.transform_private_in_expression(expr, ctx); + } _ => {} } } diff --git a/crates/oxc_transformer/src/es2022/class_properties/private_field.rs b/crates/oxc_transformer/src/es2022/class_properties/private_field.rs index 3e8138e2d3ef0..3994644ea6b9f 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/private_field.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/private_field.rs @@ -3,7 +3,7 @@ use std::mem; -use oxc_allocator::String as ArenaString; +use oxc_allocator::{Box as ArenaBox, String as ArenaString}; use oxc_ast::{ast::*, NONE}; use oxc_span::SPAN; use oxc_syntax::{reference::ReferenceId, symbol::SymbolId}; @@ -1820,6 +1820,63 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { *target = AssignmentTarget::from(replacement.into_member_expression()); } + /// Transform private field in expression. + /// + /// * Static + /// `#prop in object` -> `_checkInRHS(object) === Class` + /// + /// * Instance prop + /// `#prop in object` -> `_prop.has(_checkInRHS(object))` + /// + /// * Instance method + /// `#method in object` -> `_Class_brand.has(_checkInRHS(object))` + /// + // `#[inline]` so that compiler sees that `expr` is an `Expression::PrivateFieldExpression` + #[inline] + pub(super) fn transform_private_in_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let Expression::PrivateInExpression(private_in) = ctx.ast.move_expression(expr) else { + unreachable!(); + }; + + *expr = self.transform_private_in_expression_impl(private_in, ctx); + } + + fn transform_private_in_expression_impl( + &mut self, + private_field: ArenaBox<'a, PrivateInExpression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let PrivateInExpression { left, right, span, .. } = private_field.unbox(); + + let ResolvedPrivateProp { class_bindings, prop_binding, is_method, is_static, .. } = + self.classes_stack.find_private_prop(&left); + + if is_static { + let class_binding = class_bindings.get_or_init_static_binding(ctx); + let class_ident = class_binding.create_read_expression(ctx); + let left = self.create_check_in_rhs(right, SPAN, ctx); + return ctx.ast.expression_binary( + span, + left, + BinaryOperator::StrictEquality, + class_ident, + ); + } + + let callee = if is_method { + class_bindings.brand().create_read_expression(ctx) + } else { + prop_binding.create_read_expression(ctx) + }; + let callee = create_member_callee(callee, "has", ctx); + let argument = self.create_check_in_rhs(right, SPAN, ctx); + ctx.ast.expression_call(span, callee, NONE, ctx.ast.vec1(Argument::from(argument)), false) + } + /// Duplicate object to be used in get/set pair. /// /// If `object` may have side effects, create a temp var `_object` and assign to it. @@ -2151,4 +2208,19 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { let expressions = ctx.ast.vec_from_array([object, error]); ctx.ast.expression_sequence(span, expressions) } + + /// _checkInRHS(object) + fn create_check_in_rhs( + &self, + object: Expression<'a>, + span: Span, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + self.ctx.helper_call_expr( + Helper::CheckInRHS, + span, + ctx.ast.vec1(Argument::from(object)), + ctx, + ) + } } diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/private/accessor/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/private/accessor/output.js new file mode 100644 index 0000000000000..e0bad9fdafda2 --- /dev/null +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/private/accessor/output.js @@ -0,0 +1,10 @@ +var _Foo_brand = new WeakSet(); +class Foo { + constructor() { + babelHelpers.classPrivateMethodInitSpec(this, _Foo_brand); + } + test(other) { + return _Foo_brand.has(babelHelpers.checkInRHS(other)); + } +} +function _get_foo() {} diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/private/static-accessor/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/private/static-accessor/output.js new file mode 100644 index 0000000000000..98482c5730a20 --- /dev/null +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/private/static-accessor/output.js @@ -0,0 +1,6 @@ +class Foo { + test(other) { + return babelHelpers.checkInRHS(other) === Foo; + } +} +function _get_foo() {} diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/accessor/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/accessor/output.js new file mode 100644 index 0000000000000..e0bad9fdafda2 --- /dev/null +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/accessor/output.js @@ -0,0 +1,10 @@ +var _Foo_brand = new WeakSet(); +class Foo { + constructor() { + babelHelpers.classPrivateMethodInitSpec(this, _Foo_brand); + } + test(other) { + return _Foo_brand.has(babelHelpers.checkInRHS(other)); + } +} +function _get_foo() {} diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/half-constructed-instance/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/half-constructed-instance/output.js new file mode 100644 index 0000000000000..4bd79471ff973 --- /dev/null +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/half-constructed-instance/output.js @@ -0,0 +1,20 @@ +var _F_brand = new WeakSet(); +var _x = new WeakMap(); +var _y = new WeakMap(); +class F { + constructor() { + babelHelpers.classPrivateMethodInitSpec(this, _F_brand); + babelHelpers.classPrivateFieldInitSpec(this, _x, 0); + babelHelpers.classPrivateFieldInitSpec(this, _y, (() => { + throw "error"; + })()); + } + m() { + _F_brand.has(babelHelpers.checkInRHS(this)); + _x.has(babelHelpers.checkInRHS(this)); + _y.has(babelHelpers.checkInRHS(this)); + _F_brand.has(babelHelpers.checkInRHS(this)); + } +} +function _get_w() {} +function _z() {} diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/static-accessor/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/static-accessor/output.js new file mode 100644 index 0000000000000..98482c5730a20 --- /dev/null +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-private-property-in-object/test/fixtures/to-native-fields/static-accessor/output.js @@ -0,0 +1,6 @@ +class Foo { + test(other) { + return babelHelpers.checkInRHS(other) === Foo; + } +} +function _get_foo() {} diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index a06191d8711f4..5b2ed79859037 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,6 +1,6 @@ commit: 54a8389f -Passed: 661/1154 +Passed: 686/1154 # All Passed: * babel-plugin-transform-logical-assignment-operators @@ -811,7 +811,7 @@ x Output mismatch x Output mismatch -# babel-plugin-transform-private-property-in-object (0/59) +# babel-plugin-transform-private-property-in-object (25/59) * assumption-privateFieldsAsProperties/accessor/input.js x Output mismatch @@ -872,36 +872,6 @@ x Output mismatch * assumption-privateFieldsAsSymbols/static-method/input.js x Output mismatch -* private/accessor/input.js -x Output mismatch - -* private/field/input.js -x Output mismatch - -* private/method/input.js -x Output mismatch - -* private/native-classes/input.js -x Output mismatch - -* private/nested-class/input.js -x Output mismatch - -* private/nested-class-other-redeclared/input.js -x Output mismatch - -* private/nested-class-redeclared/input.js -x Output mismatch - -* private/static-accessor/input.js -x Output mismatch - -* private/static-field/input.js -x Output mismatch - -* private/static-method/input.js -x Output mismatch - * private/static-shadow/input.js x Output mismatch @@ -914,9 +884,6 @@ x Output mismatch * private-loose/method/input.js x Output mismatch -* private-loose/native-classes/input.js -x Output mismatch - * private-loose/nested-class/input.js x Output mismatch @@ -938,51 +905,9 @@ x Output mismatch * private-loose/static-shadow/input.js x Output mismatch -* to-native-fields/accessor/input.js -x Output mismatch - * to-native-fields/class-expression-in-default-param/input.js x Output mismatch -* to-native-fields/class-expression-instance/input.js -x Output mismatch - -* to-native-fields/class-expression-static/input.js -x Output mismatch - -* to-native-fields/field/input.js -x Output mismatch - -* to-native-fields/half-constructed-instance/input.js -x Output mismatch - -* to-native-fields/half-constructed-static/input.js -x Output mismatch - -* to-native-fields/method/input.js -x Output mismatch - -* to-native-fields/multiple-checks/input.js -x Output mismatch - -* to-native-fields/nested-class/input.js -x Output mismatch - -* to-native-fields/nested-class-other-redeclared/input.js -x Output mismatch - -* to-native-fields/nested-class-redeclared/input.js -x Output mismatch - -* to-native-fields/static-accessor/input.js -x Output mismatch - -* to-native-fields/static-field/input.js -x Output mismatch - -* to-native-fields/static-method/input.js -x Output mismatch - * to-native-fields/static-shadow/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 bbd5785280a7b..584404819bf37 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: 283 of 374 (75.67%) +Passed: 291 of 374 (77.81%) Failures: @@ -78,9 +78,6 @@ AssertionError: expected '_Class' to be 'Foo' // Object.is equality AssertionError: expected '_Class' to be 'Foo' // Object.is equality at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-public-static-infer-name-exec.test.js:9:19 -./fixtures/babel/babel-plugin-transform-class-static-block-test-fixtures-integration-loose-private-in-exec.test.js -Private field '#bar' must be declared in an enclosing class - ./fixtures/babel/babel-plugin-transform-class-static-block-test-fixtures-integration-loose-private-methods-access-exec.test.js TypeError: attempted to use private field on non-instance at _classPrivateFieldBase (./node_modules/.pnpm/@babel+runtime@7.26.0/node_modules/@babel/runtime/helpers/classPrivateFieldLooseBase.js:2:44) @@ -91,9 +88,6 @@ TypeError: attempted to use private field on non-instance AssertionError: expected [Function Base] to be undefined // Object.is equality at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-static-block-test-fixtures-integration-new-target-exec.test.js:10:29 -./fixtures/babel/babel-plugin-transform-class-static-block-test-fixtures-integration-private-in-exec.test.js -Private field '#bar' must be declared in an enclosing class - ./fixtures/babel/babel-plugin-transform-optional-chaining-test-fixtures-assumption-noDocumentAll-parenthesized-expression-member-call-exec.test.js TypeError: Cannot read properties of undefined (reading 'x') at m (./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-optional-chaining-test-fixtures-assumption-noDocumentAll-parenthesized-expression-member-call-exec.test.js:10:16) @@ -415,40 +409,32 @@ TypeError: "#privateStaticFieldValue" is write-only at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-private-methods-test-fixtures-static-accessors-privateFieldsAsSymbols-get-only-setter-exec.test.js:14:12 ./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-assumption-privateFieldsAsProperties-method-exec.test.js -Private field '#foo' must be declared in an enclosing class - -./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-assumption-privateFieldsAsProperties-rhs-not-object-exec.test.js -Private field '#p' must be declared in an enclosing class - -./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-assumption-privateFieldsAsSymbols-method-exec.test.js -Private field '#foo' must be declared in an enclosing class - -./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-assumption-privateFieldsAsSymbols-rhs-not-object-exec.test.js -Private field '#p' must be declared in an enclosing class +ReferenceError: _Foo_brand is not defined + at new Foo (./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-assumption-privateFieldsAsProperties-method-exec.test.js:8:38) + at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-assumption-privateFieldsAsProperties-method-exec.test.js:19:13 ./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-private-loose-rhs-not-object-exec.test.js -Private field '#p' must be declared in an enclosing class +AssertionError: expected [Function] to throw error including 'right-hand side of \'in\' should be a…' but got '_Class_brand is not defined' + at Proxy. (./node_modules/.pnpm/@vitest+expect@2.1.2/node_modules/@vitest/expect/dist/index.js:1438:21) + at Proxy. (./node_modules/.pnpm/@vitest+expect@2.1.2/node_modules/@vitest/expect/dist/index.js:923:17) + at Proxy.methodWrapper (./node_modules/.pnpm/chai@5.1.2/node_modules/chai/chai.js:1610:25) + at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-private-loose-rhs-not-object-exec.test.js:176:5 ./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-private-loose-static-shadow-exec.test.js -Private field '#x' must be declared in an enclosing class - -./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-private-rhs-not-object-exec.test.js -Private field '#p' must be declared in an enclosing class +AssertionError: expected 2 to be 5 // Object.is equality + at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-private-loose-static-shadow-exec.test.js:18:25 ./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-private-static-shadow-exec.test.js -Private field '#x' must be declared in an enclosing class - -./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-to-native-fields-half-constructed-instance-exec.test.js -Private field '#w' must be declared in an enclosing class +AssertionError: expected 2 to be 5 // Object.is equality + at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-private-static-shadow-exec.test.js:18:25 ./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-to-native-fields-half-constructed-static-exec.test.js -Private field '#w' must be declared in an enclosing class - -./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-to-native-fields-rhs-not-object-exec.test.js -Private field '#p' must be declared in an enclosing class +AssertionError: expected true to be false // Object.is equality + at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-to-native-fields-half-constructed-static-exec.test.js:29:15 ./fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-to-native-fields-static-shadow-exec.test.js -Private field '#x' must be declared in an enclosing class +AssertionError: expected 2 to be 5 // Object.is equality + at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-private-property-in-object-test-fixtures-to-native-fields-static-shadow-exec.test.js:18:25 ./fixtures/babel/babel-preset-env-test-fixtures-plugins-integration-issue-15170-exec.test.js AssertionError: expected [Function] to not throw an error but 'ReferenceError: x is not defined' was thrown