Skip to content

Commit

Permalink
Support transforming params of arrow functions in class fields (#13941)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Nov 9, 2021
1 parent a6a5269 commit 43f9899
Show file tree
Hide file tree
Showing 17 changed files with 168 additions and 21 deletions.
4 changes: 4 additions & 0 deletions packages/babel-plugin-transform-parameters/src/index.js
Expand Up @@ -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);
Expand Down
@@ -0,0 +1 @@
let f = (x = 0) => x + 1;
@@ -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);
@@ -0,0 +1,6 @@
{
"plugins": ["transform-parameters"],
"assumptions": {
"noNewArrows": false
}
}
@@ -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);
}
@@ -0,0 +1,5 @@
{
"plugins": [
"transform-parameters"
]
}
@@ -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);
}
@@ -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);
}
@@ -0,0 +1,5 @@
{
"plugins": [
"transform-parameters"
]
}
@@ -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);
}
@@ -0,0 +1,5 @@
class A {
#handle = (x = 0) => {
console.log(x);
};
}
@@ -0,0 +1,5 @@
{
"plugins": [
"transform-parameters"
]
}
@@ -0,0 +1,6 @@
class A {
#handle = (() => function () {
let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
console.log(x);
})();
}
@@ -0,0 +1,5 @@
class A {
handle = (x = 0) => {
console.log(x);
};
}
@@ -0,0 +1,5 @@
{
"plugins": [
"transform-parameters"
]
}
@@ -0,0 +1,6 @@
class A {
handle = (() => function () {
let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
console.log(x);
})();
}
69 changes: 48 additions & 21 deletions packages/babel-traverse/src/path/conversion.ts
Expand Up @@ -22,6 +22,7 @@ import {
stringLiteral,
super as _super,
thisExpression,
toExpression,
unaryExpression,
} from "@babel/types";
import type * as t from "@babel/types";
Expand Down Expand Up @@ -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"), [
Expand All @@ -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()],
Expand All @@ -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<t.Function>,
// TODO(Babel 8): Consider defaulting to `false` for spec compliancy
noNewArrows = true,
allowInsertArrow = true,
) {
const thisEnvFn = fnPath.findParent(p => {
): { thisBinding: string; fnPath: NodePath<t.Function> } {
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";

if (thisEnvFn.isClassProperty()) {
throw fnPath.buildCodeFrameError(
"Unable to transform arrow inside class property",
);
const inConstructor = thisEnvFn.isClassMethod({ kind: "constructor" });

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 } =
Expand Down Expand Up @@ -365,7 +392,7 @@ function hoistFunctionEnvironment(
}
}

return thisBinding;
return { thisBinding, fnPath };
}

function standardizeSuperProperty(superProp) {
Expand Down

0 comments on commit 43f9899

Please sign in to comment.