diff --git a/packages/babel-plugin-transform-parameters/src/index.js b/packages/babel-plugin-transform-parameters/src/index.js index 69d2ce512f..50048394f3 100644 --- a/packages/babel-plugin-transform-parameters/src/index.js +++ b/packages/babel-plugin-transform-parameters/src/index.js @@ -23,6 +23,10 @@ export default declare((api, options) => { ) { // default/rest visitors require access to `arguments`, so it cannot be an arrow path.arrowFunctionToExpression({ noNewArrows }); + + // In some cases arrowFunctionToExpression replaces the function with a wrapper. + // Return early; the wrapped function will be visited later in the AST traversal. + if (!path.isFunctionExpression()) return; } const convertedRest = convertFunctionRest(path); diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/default/input.js b/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/default/input.js new file mode 100644 index 0000000000..76d78dfd84 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/default/input.js @@ -0,0 +1 @@ +let f = (x = 0) => x + 1; diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/default/output.js b/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/default/output.js new file mode 100644 index 0000000000..bb87b05800 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/default/output.js @@ -0,0 +1,7 @@ +var _this = this; + +let f = function f() { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + babelHelpers.newArrowCheck(this, _this); + return x + 1; +}.bind(this); diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/options.json b/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/options.json new file mode 100644 index 0000000000..5917374834 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/assumption-noNewArrows-false/options.json @@ -0,0 +1,6 @@ +{ + "plugins": ["transform-parameters"], + "assumptions": { + "noNewArrows": false + } +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/input.js b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/input.js new file mode 100644 index 0000000000..9913f5652b --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/input.js @@ -0,0 +1,8 @@ +class A extends B { + handle = ((x = 0) => { + console.log(x, this, new.target, super.y); + })(() => { + let y = 0; + return (x = y) => x + this; + })((x = 1) => {})(this); +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/options.json b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/options.json new file mode 100644 index 0000000000..3a9f55e640 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-parameters" + ] +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/output.js b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/output.js new file mode 100644 index 0000000000..eccf75a989 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-complex/output.js @@ -0,0 +1,22 @@ +class A extends B { + handle = (() => { + var _newtarget = new.target, + _superprop_getY = () => super.y, + _this = this; + + return function () { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + console.log(x, _this, _newtarget, _superprop_getY()); + }; + })()(() => { + var _this2 = this; + + let y = 0; + return function () { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : y; + return x + _this2; + }; + })((() => function () { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; + })())(this); +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/input.js b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/input.js new file mode 100644 index 0000000000..cc76866591 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/input.js @@ -0,0 +1,8 @@ +class A extends B { + #handle = ((x = 0) => { + console.log(x, this, new.target, super.y); + })(() => { + let y = 0; + return (x = y) => x + this; + })((x = 1) => {})(this); +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/options.json b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/options.json new file mode 100644 index 0000000000..3a9f55e640 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-parameters" + ] +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/output.js b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/output.js new file mode 100644 index 0000000000..fb2fda678c --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private-complex/output.js @@ -0,0 +1,22 @@ +class A extends B { + #handle = (() => { + var _newtarget = new.target, + _superprop_getY = () => super.y, + _this = this; + + return function () { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + console.log(x, _this, _newtarget, _superprop_getY()); + }; + })()(() => { + var _this2 = this; + + let y = 0; + return function () { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : y; + return x + _this2; + }; + })((() => function () { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; + })())(this); +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/input.js b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/input.js new file mode 100644 index 0000000000..5a2a264ab5 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/input.js @@ -0,0 +1,5 @@ +class A { + #handle = (x = 0) => { + console.log(x); + }; +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/options.json b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/options.json new file mode 100644 index 0000000000..3a9f55e640 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-parameters" + ] +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/output.js b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/output.js new file mode 100644 index 0000000000..bbaf1ced39 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939-private/output.js @@ -0,0 +1,6 @@ +class A { + #handle = (() => function () { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + console.log(x); + })(); +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/input.js b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/input.js new file mode 100644 index 0000000000..cbf2dccabf --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/input.js @@ -0,0 +1,5 @@ +class A { + handle = (x = 0) => { + console.log(x); + }; +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/options.json b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/options.json new file mode 100644 index 0000000000..3a9f55e640 --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-parameters" + ] +} diff --git a/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/output.js b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/output.js new file mode 100644 index 0000000000..e3a280153e --- /dev/null +++ b/packages/babel-plugin-transform-parameters/test/fixtures/regression/13939/output.js @@ -0,0 +1,6 @@ +class A { + handle = (() => function () { + let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + console.log(x); + })(); +} diff --git a/packages/babel-traverse/src/path/conversion.ts b/packages/babel-traverse/src/path/conversion.ts index 85b03a3156..5f8d9a0250 100644 --- a/packages/babel-traverse/src/path/conversion.ts +++ b/packages/babel-traverse/src/path/conversion.ts @@ -22,6 +22,7 @@ import { stringLiteral, super as _super, thisExpression, + toExpression, unaryExpression, } from "@babel/types"; import type * as t from "@babel/types"; @@ -146,27 +147,26 @@ export function arrowFunctionToExpression( ); } - const thisBinding = hoistFunctionEnvironment( + const { thisBinding, fnPath: fn } = hoistFunctionEnvironment( this, noNewArrows, allowInsertArrow, ); - this.ensureBlock(); - // @ts-expect-error todo(flow->ts): avoid mutating nodes - this.node.type = "FunctionExpression"; + fn.ensureBlock(); + fn.node.type = "FunctionExpression"; if (!noNewArrows) { const checkBinding = thisBinding ? null - : this.parentPath.scope.generateUidIdentifier("arrowCheckId"); + : fn.scope.generateUidIdentifier("arrowCheckId"); if (checkBinding) { - this.parentPath.scope.push({ + fn.parentPath.scope.push({ id: checkBinding, init: objectExpression([]), }); } - this.get("body").unshiftContainer( + fn.get("body").unshiftContainer( "body", expressionStatement( callExpression(this.hub.addHelper("newArrowCheck"), [ @@ -178,10 +178,10 @@ export function arrowFunctionToExpression( ), ); - this.replaceWith( + fn.replaceWith( callExpression( memberExpression( - nameFunction(this, true) || this.node, + nameFunction(this, true) || fn.node, identifier("bind"), ), [checkBinding ? identifier(checkBinding.name) : thisExpression()], @@ -193,26 +193,53 @@ export function arrowFunctionToExpression( /** * Given a function, traverse its contents, and if there are references to "this", "arguments", "super", * or "new.target", ensure that these references reference the parent environment around this function. + * + * @returns `thisBinding`: the name of the injected reference to `this`; for example "_this" + * @returns `fnPath`: the new path to the function node. This is different from the fnPath + * parameter when the function node is wrapped in another node. */ function hoistFunctionEnvironment( - fnPath, + fnPath: NodePath, // TODO(Babel 8): Consider defaulting to `false` for spec compliancy noNewArrows = true, allowInsertArrow = true, -) { - const thisEnvFn = fnPath.findParent(p => { +): { thisBinding: string; fnPath: NodePath } { + let arrowParent; + let thisEnvFn = fnPath.findParent(p => { + if (p.isArrowFunctionExpression()) { + arrowParent ??= p; + return false; + } return ( - (p.isFunction() && !p.isArrowFunctionExpression()) || + p.isFunction() || p.isProgram() || - p.isClassProperty({ static: false }) + p.isClassProperty({ static: false }) || + p.isClassPrivateProperty({ static: false }) ); }); - const inConstructor = thisEnvFn?.node.kind === "constructor"; + const inConstructor = thisEnvFn.isClassMethod({ kind: "constructor" }); - if (thisEnvFn.isClassProperty()) { - throw fnPath.buildCodeFrameError( - "Unable to transform arrow inside class property", - ); + if (thisEnvFn.isClassProperty() || thisEnvFn.isClassPrivateProperty()) { + if (arrowParent) { + thisEnvFn = arrowParent; + } else if (allowInsertArrow) { + // It's safe to wrap this function in another and not hoist to the + // top level because the 'this' binding is constant in class + // properties (since 'super()' has already been called), so we don't + // need to capture/reassign it at the top level. + fnPath.replaceWith( + callExpression( + arrowFunctionExpression([], toExpression(fnPath.node)), + [], + ), + ); + thisEnvFn = fnPath.get("callee"); + fnPath = thisEnvFn.get("body"); + } else { + throw fnPath.buildCodeFrameError( + "Unable to transform arrow inside class property", + ); + } } const { thisPaths, argumentsPaths, newTargetPaths, superProps, superCalls } = @@ -365,7 +392,7 @@ function hoistFunctionEnvironment( } } - return thisBinding; + return { thisBinding, fnPath }; } function standardizeSuperProperty(superProp) {