diff --git a/packages/babel-helper-member-expression-to-functions/src/index.js b/packages/babel-helper-member-expression-to-functions/src/index.js index 62d844b9f177..0e5ef5ecb8e1 100644 --- a/packages/babel-helper-member-expression-to-functions/src/index.js +++ b/packages/babel-helper-member-expression-to-functions/src/index.js @@ -84,7 +84,7 @@ const handle = { }, handle(member) { - const { node, parent, parentPath } = member; + const { node, parent, parentPath, scope } = member; if (member.isOptionalMemberExpression()) { // Transforming optional chaining requires we replace ancestors. @@ -118,6 +118,17 @@ const handle = { return true; }); + // Replace `function (a, x = a.b?.#c) {}` to `function (a, x = (() => a.b?.#c)() ){}` + // so the temporary variable can be injected in correct scope + // This can be further optimized to avoid unecessary IIFE + if (scope.path.isPattern()) { + endPath.replaceWith( + // The injected member will be queued and eventually transformed when visited + t.callExpression(t.arrowFunctionExpression([], endPath.node), []), + ); + return; + } + const rootParentPath = endPath.parentPath; if ( rootParentPath.isUpdateExpression({ argument: node }) || @@ -165,7 +176,6 @@ const handle = { ); } - const { scope } = member; const startingProp = startingOptional.isOptionalMemberExpression() ? "object" : "callee"; @@ -362,6 +372,16 @@ const handle = { // MEMBER?.(ARGS) -> _optionalCall(MEMBER, ARGS) if (parentPath.isOptionalCallExpression({ callee: node })) { + // Replace `function (a, x = a.b.#c?.()) {}` to `function (a, x = (() => a.b.#c?.())() ){}` + // so the temporary variable can be injected in correct scope + // This can be further optimized to avoid unecessary IIFE + if (scope.path.isPattern()) { + parentPath.replaceWith( + // The injected member will be queued and eventually transformed when visited + t.callExpression(t.arrowFunctionExpression([], parentPath.node), []), + ); + return; + } parentPath.replaceWith(this.optionalCall(member, parent.arguments)); return; } diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/exec.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/exec.js new file mode 100644 index 000000000000..6eb26ab6e01a --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/exec.js @@ -0,0 +1,46 @@ +class Foo { + static #x = 1; + static #m = function() { return this.#x; }; + static #self = Foo; + static self = Foo; + static getSelf() { return this } + + static test() { + const o = { Foo: Foo }; + const deep = { very: { o } }; + function fn() { + return o; + } + function fnDeep() { + return deep; + } + + function f(o, r = o?.Foo.#m()) { + return r; + } + + function g(o, r = o?.Foo.#self.getSelf().#m()) { + return r; + } + + function h(fnDeep, r = fnDeep?.().very.o?.Foo?.#m()) { + return r; + } + + function i(fn, r = fn?.().Foo.#self?.getSelf()?.self.#m()) { + return r; + } + + function j(fn, r = (fn().Foo.#self.getSelf().self.#m)?.()) { + return r; + } + + expect(f(o)).toBe(1); + expect(g(o)).toBe(1); + expect(h(fnDeep)).toBe(1); + expect(i(fn)).toBe(1); + expect(j(fn)).toBe(1); + } +} + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/input.js new file mode 100644 index 000000000000..c5fe33636eca --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/input.js @@ -0,0 +1,46 @@ +class Foo { + static #x = 1; + static #m = function() { return this.#x; }; + static #self = Foo; + static self = Foo; + static getSelf() { return this } + + static test() { + const o = { Foo: Foo }; + const deep = { very: { o } }; + function fn() { + return o; + } + function fnDeep() { + return deep; + } + + function f(o, r = o?.Foo.#m()) { + return r; + } + + function g(o, r = o?.Foo.#self.getSelf().#m?.()) { + return r; + } + + function h(fnDeep, r = fnDeep?.().very.o?.Foo?.#m()) { + return r; + } + + function i(fn, r = fn?.().Foo.#self?.getSelf()?.self.#m()) { + return r; + } + + function j(fn, r = (fn().Foo.#self.getSelf().self.#m)?.()) { + return r; + } + + f(o); + g(o); + h(fnDeep); + i(fn); + j(fn); + } +} + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/options.json new file mode 100644 index 000000000000..124133b5af2b --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/options.json @@ -0,0 +1,6 @@ +{ + "plugins": [ + ["proposal-optional-chaining", { "loose": true }], + ["proposal-class-properties", { "loose": true }] + ] +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/output.js new file mode 100644 index 000000000000..fb3516c4b811 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param-with-transform/output.js @@ -0,0 +1,96 @@ +function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } + +var id = 0; + +function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; } + +var _x = _classPrivateFieldLooseKey("x"); + +var _m = _classPrivateFieldLooseKey("m"); + +var _self = _classPrivateFieldLooseKey("self"); + +class Foo { + static getSelf() { + return this; + } + + static test() { + const o = { + Foo: Foo + }; + const deep = { + very: { + o + } + }; + + function fn() { + return o; + } + + function fnDeep() { + return deep; + } + + function f(o, r = (() => o === null || o === void 0 ? void 0 : _classPrivateFieldLooseBase(o.Foo, _m)[_m]())()) { + return r; + } + + function g(o, r = (() => { + var _ref; + + return (_ref = (() => o === null || o === void 0 ? void 0 : _classPrivateFieldLooseBase(_classPrivateFieldLooseBase(o.Foo, _self)[_self].getSelf(), _m)[_m])()) == null ? void 0 : _ref(); + })()) { + return r; + } + + function h(fnDeep, r = (() => { + var _fnDeep$very$o$Foo, _fnDeep$very$o; + + return (_fnDeep$very$o$Foo = fnDeep == null ? void 0 : (_fnDeep$very$o = fnDeep().very.o) == null ? void 0 : _fnDeep$very$o.Foo) === null || _fnDeep$very$o$Foo === void 0 ? void 0 : _classPrivateFieldLooseBase(_fnDeep$very$o$Foo, _m)[_m](); + })()) { + return r; + } + + function i(fn, r = (() => { + var _getSelf, _ref2; + + return (_getSelf = (_ref2 = (() => fn === null || fn === void 0 ? void 0 : _classPrivateFieldLooseBase(fn().Foo, _self)[_self])()) == null ? void 0 : _ref2.getSelf()) === null || _getSelf === void 0 ? void 0 : _classPrivateFieldLooseBase(_getSelf.self, _m)[_m](); + })()) { + return r; + } + + function j(fn, r = (() => { + var _classPrivateFieldLoo, _classPrivateFieldLoo2; + + return (_classPrivateFieldLoo = (_classPrivateFieldLoo2 = _classPrivateFieldLooseBase(_classPrivateFieldLooseBase(fn().Foo, _self)[_self].getSelf().self, _m))[_m]) == null ? void 0 : _classPrivateFieldLoo.call(_classPrivateFieldLoo2); + })()) { + return r; + } + + f(o); + g(o); + h(fnDeep); + i(fn); + j(fn); + } + +} + +Object.defineProperty(Foo, _x, { + writable: true, + value: 1 +}); +Object.defineProperty(Foo, _m, { + writable: true, + value: function () { + return _classPrivateFieldLooseBase(this, _x)[_x]; + } +}); +Object.defineProperty(Foo, _self, { + writable: true, + value: Foo +}); +Foo.self = Foo; +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/exec.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/exec.js new file mode 100644 index 000000000000..6eb26ab6e01a --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/exec.js @@ -0,0 +1,46 @@ +class Foo { + static #x = 1; + static #m = function() { return this.#x; }; + static #self = Foo; + static self = Foo; + static getSelf() { return this } + + static test() { + const o = { Foo: Foo }; + const deep = { very: { o } }; + function fn() { + return o; + } + function fnDeep() { + return deep; + } + + function f(o, r = o?.Foo.#m()) { + return r; + } + + function g(o, r = o?.Foo.#self.getSelf().#m()) { + return r; + } + + function h(fnDeep, r = fnDeep?.().very.o?.Foo?.#m()) { + return r; + } + + function i(fn, r = fn?.().Foo.#self?.getSelf()?.self.#m()) { + return r; + } + + function j(fn, r = (fn().Foo.#self.getSelf().self.#m)?.()) { + return r; + } + + expect(f(o)).toBe(1); + expect(g(o)).toBe(1); + expect(h(fnDeep)).toBe(1); + expect(i(fn)).toBe(1); + expect(j(fn)).toBe(1); + } +} + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/input.js new file mode 100644 index 000000000000..c5fe33636eca --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/input.js @@ -0,0 +1,46 @@ +class Foo { + static #x = 1; + static #m = function() { return this.#x; }; + static #self = Foo; + static self = Foo; + static getSelf() { return this } + + static test() { + const o = { Foo: Foo }; + const deep = { very: { o } }; + function fn() { + return o; + } + function fnDeep() { + return deep; + } + + function f(o, r = o?.Foo.#m()) { + return r; + } + + function g(o, r = o?.Foo.#self.getSelf().#m?.()) { + return r; + } + + function h(fnDeep, r = fnDeep?.().very.o?.Foo?.#m()) { + return r; + } + + function i(fn, r = fn?.().Foo.#self?.getSelf()?.self.#m()) { + return r; + } + + function j(fn, r = (fn().Foo.#self.getSelf().self.#m)?.()) { + return r; + } + + f(o); + g(o); + h(fnDeep); + i(fn); + j(fn); + } +} + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/options.json new file mode 100644 index 000000000000..2d5cfe8e8095 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/options.json @@ -0,0 +1,4 @@ +{ + "plugins": [["proposal-class-properties", { "loose": true }]], + "minNodeVersion": "14.0.0" +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/output.js new file mode 100644 index 000000000000..2b7d864e3b04 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/optional-chain-in-function-param/output.js @@ -0,0 +1,88 @@ +function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } + +var id = 0; + +function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; } + +var _x = _classPrivateFieldLooseKey("x"); + +var _m = _classPrivateFieldLooseKey("m"); + +var _self = _classPrivateFieldLooseKey("self"); + +class Foo { + static getSelf() { + return this; + } + + static test() { + const o = { + Foo: Foo + }; + const deep = { + very: { + o + } + }; + + function fn() { + return o; + } + + function fnDeep() { + return deep; + } + + function f(o, r = (() => o === null || o === void 0 ? void 0 : _classPrivateFieldLooseBase(o.Foo, _m)[_m]())()) { + return r; + } + + function g(o, r = (() => o === null || o === void 0 ? void 0 : _classPrivateFieldLooseBase(_classPrivateFieldLooseBase(o.Foo, _self)[_self].getSelf(), _m)[_m])()?.()) { + return r; + } + + function h(fnDeep, r = (() => { + var _fnDeep$very$o$Foo; + + return (_fnDeep$very$o$Foo = fnDeep?.().very.o?.Foo) === null || _fnDeep$very$o$Foo === void 0 ? void 0 : _classPrivateFieldLooseBase(_fnDeep$very$o$Foo, _m)[_m](); + })()) { + return r; + } + + function i(fn, r = (() => { + var _getSelf; + + return (_getSelf = (() => fn === null || fn === void 0 ? void 0 : _classPrivateFieldLooseBase(fn().Foo, _self)[_self])()?.getSelf()) === null || _getSelf === void 0 ? void 0 : _classPrivateFieldLooseBase(_getSelf.self, _m)[_m](); + })()) { + return r; + } + + function j(fn, r = (() => _classPrivateFieldLooseBase(_classPrivateFieldLooseBase(fn().Foo, _self)[_self].getSelf().self, _m)[_m]?.())()) { + return r; + } + + f(o); + g(o); + h(fnDeep); + i(fn); + j(fn); + } + +} + +Object.defineProperty(Foo, _x, { + writable: true, + value: 1 +}); +Object.defineProperty(Foo, _m, { + writable: true, + value: function () { + return _classPrivateFieldLooseBase(this, _x)[_x]; + } +}); +Object.defineProperty(Foo, _self, { + writable: true, + value: Foo +}); +Foo.self = Foo; +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/exec.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/exec.js new file mode 100644 index 000000000000..6eb26ab6e01a --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/exec.js @@ -0,0 +1,46 @@ +class Foo { + static #x = 1; + static #m = function() { return this.#x; }; + static #self = Foo; + static self = Foo; + static getSelf() { return this } + + static test() { + const o = { Foo: Foo }; + const deep = { very: { o } }; + function fn() { + return o; + } + function fnDeep() { + return deep; + } + + function f(o, r = o?.Foo.#m()) { + return r; + } + + function g(o, r = o?.Foo.#self.getSelf().#m()) { + return r; + } + + function h(fnDeep, r = fnDeep?.().very.o?.Foo?.#m()) { + return r; + } + + function i(fn, r = fn?.().Foo.#self?.getSelf()?.self.#m()) { + return r; + } + + function j(fn, r = (fn().Foo.#self.getSelf().self.#m)?.()) { + return r; + } + + expect(f(o)).toBe(1); + expect(g(o)).toBe(1); + expect(h(fnDeep)).toBe(1); + expect(i(fn)).toBe(1); + expect(j(fn)).toBe(1); + } +} + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/input.js new file mode 100644 index 000000000000..c5fe33636eca --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/input.js @@ -0,0 +1,46 @@ +class Foo { + static #x = 1; + static #m = function() { return this.#x; }; + static #self = Foo; + static self = Foo; + static getSelf() { return this } + + static test() { + const o = { Foo: Foo }; + const deep = { very: { o } }; + function fn() { + return o; + } + function fnDeep() { + return deep; + } + + function f(o, r = o?.Foo.#m()) { + return r; + } + + function g(o, r = o?.Foo.#self.getSelf().#m?.()) { + return r; + } + + function h(fnDeep, r = fnDeep?.().very.o?.Foo?.#m()) { + return r; + } + + function i(fn, r = fn?.().Foo.#self?.getSelf()?.self.#m()) { + return r; + } + + function j(fn, r = (fn().Foo.#self.getSelf().self.#m)?.()) { + return r; + } + + f(o); + g(o); + h(fnDeep); + i(fn); + j(fn); + } +} + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/options.json new file mode 100644 index 000000000000..63b4c77cc8e8 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["proposal-optional-chaining", "proposal-class-properties"] +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/output.js new file mode 100644 index 000000000000..1c52675c3085 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param-with-transform/output.js @@ -0,0 +1,94 @@ +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } + +class Foo { + static getSelf() { + return this; + } + + static test() { + const o = { + Foo: Foo + }; + const deep = { + very: { + o + } + }; + + function fn() { + return o; + } + + function fnDeep() { + return deep; + } + + function f(o, r = (() => { + var _o$Foo; + + return o === null || o === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(_o$Foo = o.Foo, Foo, _m).call(_o$Foo); + })()) { + return r; + } + + function g(o, r = (() => { + var _ref; + + return (_ref = (() => o === null || o === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(_classStaticPrivateFieldSpecGet(o.Foo, Foo, _self).getSelf(), Foo, _m))()) === null || _ref === void 0 ? void 0 : _ref(); + })()) { + return r; + } + + function h(fnDeep, r = (() => { + var _fnDeep$very$o$Foo, _fnDeep$very$o; + + return (_fnDeep$very$o$Foo = fnDeep === null || fnDeep === void 0 ? void 0 : (_fnDeep$very$o = fnDeep().very.o) === null || _fnDeep$very$o === void 0 ? void 0 : _fnDeep$very$o.Foo) === null || _fnDeep$very$o$Foo === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(_fnDeep$very$o$Foo, Foo, _m).call(_fnDeep$very$o$Foo); + })()) { + return r; + } + + function i(fn, r = (() => { + var _getSelf, _getSelf$self, _ref2; + + return (_getSelf = (_ref2 = (() => fn === null || fn === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(fn().Foo, Foo, _self))()) === null || _ref2 === void 0 ? void 0 : _ref2.getSelf()) === null || _getSelf === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(_getSelf$self = _getSelf.self, Foo, _m).call(_getSelf$self); + })()) { + return r; + } + + function j(fn, r = (() => { + var _classStaticPrivateFi, _classStaticPrivateFi2; + + return (_classStaticPrivateFi2 = _classStaticPrivateFieldSpecGet(_classStaticPrivateFi = _classStaticPrivateFieldSpecGet(fn().Foo, Foo, _self).getSelf().self, Foo, _m)) === null || _classStaticPrivateFi2 === void 0 ? void 0 : _classStaticPrivateFi2.call(_classStaticPrivateFi); + })()) { + return r; + } + + f(o); + g(o); + h(fnDeep); + i(fn); + j(fn); + } + +} + +var _x = { + writable: true, + value: 1 +}; +var _m = { + writable: true, + value: function () { + return _classStaticPrivateFieldSpecGet(this, Foo, _x); + } +}; +var _self = { + writable: true, + value: Foo +}; + +_defineProperty(Foo, "self", Foo); + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/exec.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/exec.js new file mode 100644 index 000000000000..6eb26ab6e01a --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/exec.js @@ -0,0 +1,46 @@ +class Foo { + static #x = 1; + static #m = function() { return this.#x; }; + static #self = Foo; + static self = Foo; + static getSelf() { return this } + + static test() { + const o = { Foo: Foo }; + const deep = { very: { o } }; + function fn() { + return o; + } + function fnDeep() { + return deep; + } + + function f(o, r = o?.Foo.#m()) { + return r; + } + + function g(o, r = o?.Foo.#self.getSelf().#m()) { + return r; + } + + function h(fnDeep, r = fnDeep?.().very.o?.Foo?.#m()) { + return r; + } + + function i(fn, r = fn?.().Foo.#self?.getSelf()?.self.#m()) { + return r; + } + + function j(fn, r = (fn().Foo.#self.getSelf().self.#m)?.()) { + return r; + } + + expect(f(o)).toBe(1); + expect(g(o)).toBe(1); + expect(h(fnDeep)).toBe(1); + expect(i(fn)).toBe(1); + expect(j(fn)).toBe(1); + } +} + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/input.js new file mode 100644 index 000000000000..c5fe33636eca --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/input.js @@ -0,0 +1,46 @@ +class Foo { + static #x = 1; + static #m = function() { return this.#x; }; + static #self = Foo; + static self = Foo; + static getSelf() { return this } + + static test() { + const o = { Foo: Foo }; + const deep = { very: { o } }; + function fn() { + return o; + } + function fnDeep() { + return deep; + } + + function f(o, r = o?.Foo.#m()) { + return r; + } + + function g(o, r = o?.Foo.#self.getSelf().#m?.()) { + return r; + } + + function h(fnDeep, r = fnDeep?.().very.o?.Foo?.#m()) { + return r; + } + + function i(fn, r = fn?.().Foo.#self?.getSelf()?.self.#m()) { + return r; + } + + function j(fn, r = (fn().Foo.#self.getSelf().self.#m)?.()) { + return r; + } + + f(o); + g(o); + h(fnDeep); + i(fn); + j(fn); + } +} + +Foo.test(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/options.json new file mode 100644 index 000000000000..3b59e1bbfcc8 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/options.json @@ -0,0 +1,4 @@ +{ + "plugins": ["proposal-class-properties"], + "minNodeVersion": "14.0.0" +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/output.js new file mode 100644 index 000000000000..782d93a5793a --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/optional-chain-in-function-param/output.js @@ -0,0 +1,90 @@ +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } + +class Foo { + static getSelf() { + return this; + } + + static test() { + const o = { + Foo: Foo + }; + const deep = { + very: { + o + } + }; + + function fn() { + return o; + } + + function fnDeep() { + return deep; + } + + function f(o, r = (() => { + var _o$Foo; + + return o === null || o === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(_o$Foo = o.Foo, Foo, _m).call(_o$Foo); + })()) { + return r; + } + + function g(o, r = (() => o === null || o === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(_classStaticPrivateFieldSpecGet(o.Foo, Foo, _self).getSelf(), Foo, _m))()?.()) { + return r; + } + + function h(fnDeep, r = (() => { + var _fnDeep$very$o$Foo; + + return (_fnDeep$very$o$Foo = fnDeep?.().very.o?.Foo) === null || _fnDeep$very$o$Foo === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(_fnDeep$very$o$Foo, Foo, _m).call(_fnDeep$very$o$Foo); + })()) { + return r; + } + + function i(fn, r = (() => { + var _getSelf, _getSelf$self; + + return (_getSelf = (() => fn === null || fn === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(fn().Foo, Foo, _self))()?.getSelf()) === null || _getSelf === void 0 ? void 0 : _classStaticPrivateFieldSpecGet(_getSelf$self = _getSelf.self, Foo, _m).call(_getSelf$self); + })()) { + return r; + } + + function j(fn, r = (() => { + var _classStaticPrivateFi; + + return _classStaticPrivateFieldSpecGet(_classStaticPrivateFi = _classStaticPrivateFieldSpecGet(fn().Foo, Foo, _self).getSelf().self, Foo, _m)?.call(_classStaticPrivateFi); + })()) { + return r; + } + + f(o); + g(o); + h(fnDeep); + i(fn); + j(fn); + } + +} + +var _x = { + writable: true, + value: 1 +}; +var _m = { + writable: true, + value: function () { + return _classStaticPrivateFieldSpecGet(this, Foo, _x); + } +}; +var _self = { + writable: true, + value: Foo +}; + +_defineProperty(Foo, "self", Foo); + +Foo.test(); diff --git a/packages/babel-plugin-proposal-nullish-coalescing-operator/src/index.js b/packages/babel-plugin-proposal-nullish-coalescing-operator/src/index.js index 848a6c3ee3cd..12ddbc7cba97 100644 --- a/packages/babel-plugin-proposal-nullish-coalescing-operator/src/index.js +++ b/packages/babel-plugin-proposal-nullish-coalescing-operator/src/index.js @@ -1,6 +1,6 @@ import { declare } from "@babel/helper-plugin-utils"; import syntaxNullishCoalescingOperator from "@babel/plugin-syntax-nullish-coalescing-operator"; -import { types as t } from "@babel/core"; +import { types as t, template } from "@babel/core"; export default declare((api, { loose = false }) => { api.assertVersion(7); @@ -16,13 +16,21 @@ export default declare((api, { loose = false }) => { return; } - let ref = scope.maybeGenerateMemoised(node.left); + let ref; let assignment; // skip creating extra reference when `left` is static - if (ref === null) { + if (scope.isStatic(node.left)) { ref = node.left; assignment = t.cloneNode(node.left); + } else if (scope.path.isPattern()) { + // Replace `function (a, x = a.b ?? c) {}` to `function (a, x = (() => a.b ?? c)() ){}` + // so the temporary variable can be injected in correct scope + path.replaceWith(template.ast`(() => ${path.node})()`); + // The injected nullish expression will be queued and eventually transformed when visited + return; } else { + ref = scope.generateUidIdentifierBasedOnNode(node.left); + scope.push({ id: t.cloneNode(ref) }); assignment = t.assignmentExpression("=", ref, node.left); } diff --git a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-destructuring/input.js b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-destructuring/input.js new file mode 100644 index 000000000000..876e05c1a625 --- /dev/null +++ b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-destructuring/input.js @@ -0,0 +1 @@ +var { qux = foo.bar ?? "qux" } = {}; diff --git a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default/options.json b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-destructuring/options.json similarity index 100% rename from packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default/options.json rename to packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-destructuring/options.json diff --git a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-destructuring/output.js b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-destructuring/output.js new file mode 100644 index 000000000000..e6dcd5e32e2c --- /dev/null +++ b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-destructuring/output.js @@ -0,0 +1,5 @@ +var _foo$bar; + +var { + qux = (_foo$bar = foo.bar) !== null && _foo$bar !== void 0 ? _foo$bar : "qux" +} = {}; diff --git a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default/input.js b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/input.js similarity index 51% rename from packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default/input.js rename to packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/input.js index 0c13164800af..65b7719722bc 100644 --- a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default/input.js +++ b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/input.js @@ -1 +1,3 @@ function foo(foo, qux = foo.bar ?? "qux") {} + +function bar(bar, qux = bar ?? "qux") {} diff --git a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/options.json b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/options.json new file mode 100644 index 000000000000..47e490d2afcf --- /dev/null +++ b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["proposal-nullish-coalescing-operator"] +} diff --git a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/output.js b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/output.js new file mode 100644 index 000000000000..b746a752e57a --- /dev/null +++ b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default-param/output.js @@ -0,0 +1,7 @@ +function foo(foo, qux = (() => { + var _foo$bar; + + return (_foo$bar = foo.bar) !== null && _foo$bar !== void 0 ? _foo$bar : "qux"; +})()) {} + +function bar(bar, qux = bar !== null && bar !== void 0 ? bar : "qux") {} diff --git a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default/output.js b/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default/output.js deleted file mode 100644 index 2b2f2314b366..000000000000 --- a/packages/babel-plugin-proposal-nullish-coalescing-operator/test/fixtures/nullish-coalescing/transform-in-default/output.js +++ /dev/null @@ -1,3 +0,0 @@ -function foo(foo, qux = (_foo$bar = foo.bar) !== null && _foo$bar !== void 0 ? _foo$bar : "qux") { - var _foo$bar; -} diff --git a/packages/babel-plugin-proposal-optional-chaining/src/index.js b/packages/babel-plugin-proposal-optional-chaining/src/index.js index f3d32be4338a..f9f8f99ab84d 100644 --- a/packages/babel-plugin-proposal-optional-chaining/src/index.js +++ b/packages/babel-plugin-proposal-optional-chaining/src/index.js @@ -4,7 +4,7 @@ import { skipTransparentExprWrappers, } from "@babel/helper-skip-transparent-expression-wrappers"; import syntaxOptionalChaining from "@babel/plugin-syntax-optional-chaining"; -import { types as t } from "@babel/core"; +import { types as t, template } from "@babel/core"; export default declare((api, options) => { api.assertVersion(7); @@ -22,6 +22,31 @@ export default declare((api, options) => { ); } + /** + * Test if a given optional chain `path` needs to be memoized + * @param {NodePath} path + * @returns {boolean} + */ + function needsMemoize(path) { + let optionalPath = path; + const { scope } = path; + while ( + optionalPath.isOptionalMemberExpression() || + optionalPath.isOptionalCallExpression() + ) { + const { node } = optionalPath; + const childKey = optionalPath.isOptionalMemberExpression() + ? "object" + : "callee"; + const childPath = skipTransparentExprWrappers(optionalPath.get(childKey)); + if (node.optional) { + return !scope.isStatic(childPath.node); + } + + optionalPath = childPath; + } + } + return { name: "proposal-optional-chaining", inherits: syntaxOptionalChaining, @@ -46,6 +71,13 @@ export default declare((api, options) => { const optionals = []; let optionalPath = path; + // Replace `function (a, x = a.b?.c) {}` to `function (a, x = (() => a.b?.c)() ){}` + // so the temporary variable can be injected in correct scope + if (scope.path.isPattern() && needsMemoize(optionalPath)) { + path.replaceWith(template.ast`(() => ${path.node})()`); + // The injected optional chain will be queued and eventually transformed when visited + return; + } while ( optionalPath.isOptionalMemberExpression() || optionalPath.isOptionalCallExpression() diff --git a/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/input.js b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/input.js new file mode 100644 index 000000000000..882511b447f8 --- /dev/null +++ b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/input.js @@ -0,0 +1,9 @@ +function f(a = x?.y) {} + +function g({ a, b = a?.c }) {} + +function h(a, { b = a.b?.c?.d.e }) {} + +function i(a, { b = (a.b?.c?.d).e }) {} + +function j(a, { b = a?.b?.c().d.e }) {} diff --git a/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/options.json b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/options.json new file mode 100644 index 000000000000..39ea3f99c7ff --- /dev/null +++ b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["proposal-optional-chaining", { "loose": true }]] +} diff --git a/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/output.js b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/output.js new file mode 100644 index 000000000000..f5cda260cbfe --- /dev/null +++ b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params-loose/output.js @@ -0,0 +1,34 @@ +function f(a = (() => { + var _x; + + return (_x = x) == null ? void 0 : _x.y; +})()) {} + +function g({ + a, + b = a == null ? void 0 : a.c +}) {} + +function h(a, { + b = (() => { + var _a$b, _a$b$c; + + return (_a$b = a.b) == null ? void 0 : (_a$b$c = _a$b.c) == null ? void 0 : _a$b$c.d.e; + })() +}) {} + +function i(a, { + b = (() => { + var _a$b2, _a$b2$c; + + return (_a$b2 = a.b) == null ? void 0 : (_a$b2$c = _a$b2.c) == null ? void 0 : _a$b2$c.d; + })().e +}) {} + +function j(a, { + b = (() => { + var _a$b3; + + return a == null ? void 0 : (_a$b3 = a.b) == null ? void 0 : _a$b3.c().d.e; + })() +}) {} diff --git a/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params/input.js b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params/input.js new file mode 100644 index 000000000000..882511b447f8 --- /dev/null +++ b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params/input.js @@ -0,0 +1,9 @@ +function f(a = x?.y) {} + +function g({ a, b = a?.c }) {} + +function h(a, { b = a.b?.c?.d.e }) {} + +function i(a, { b = (a.b?.c?.d).e }) {} + +function j(a, { b = a?.b?.c().d.e }) {} diff --git a/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params/output.js b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params/output.js new file mode 100644 index 000000000000..8d7909dc76fa --- /dev/null +++ b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-function-params/output.js @@ -0,0 +1,34 @@ +function f(a = (() => { + var _x; + + return (_x = x) === null || _x === void 0 ? void 0 : _x.y; +})()) {} + +function g({ + a, + b = a === null || a === void 0 ? void 0 : a.c +}) {} + +function h(a, { + b = (() => { + var _a$b, _a$b$c; + + return (_a$b = a.b) === null || _a$b === void 0 ? void 0 : (_a$b$c = _a$b.c) === null || _a$b$c === void 0 ? void 0 : _a$b$c.d.e; + })() +}) {} + +function i(a, { + b = (() => { + var _a$b2, _a$b2$c; + + return (_a$b2 = a.b) === null || _a$b2 === void 0 ? void 0 : (_a$b2$c = _a$b2.c) === null || _a$b2$c === void 0 ? void 0 : _a$b2$c.d; + })().e +}) {} + +function j(a, { + b = (() => { + var _a$b3; + + return a === null || a === void 0 ? void 0 : (_a$b3 = a.b) === null || _a$b3 === void 0 ? void 0 : _a$b3.c().d.e; + })() +}) {} diff --git a/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-var-destructuring/input.js b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-var-destructuring/input.js new file mode 100644 index 000000000000..d88b60a0e957 --- /dev/null +++ b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-var-destructuring/input.js @@ -0,0 +1 @@ +var { a = x?.y } = {}; diff --git a/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-var-destructuring/output.js b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-var-destructuring/output.js new file mode 100644 index 000000000000..0e70d31a12bf --- /dev/null +++ b/packages/babel-plugin-proposal-optional-chaining/test/fixtures/general/in-var-destructuring/output.js @@ -0,0 +1,5 @@ +var _x; + +var { + a = (_x = x) === null || _x === void 0 ? void 0 : _x.y +} = {};