Kill the "shadow-functions.js" internal plugin in favor of an explicit helper (#5677)

* Handle arrow function processing via shared API rather than default plugin.

* Fix a few small PR comments.

* Preserve existing spec arrow 'this' rewrites, and support spec in subclass constructors.
This commit is contained in:
Logan Smyth
2017-05-05 13:27:18 -07:00
committed by GitHub
parent d5aa6d3ff8
commit 14584c218c
33 changed files with 1089 additions and 342 deletions

View File

@@ -17,7 +17,6 @@ import buildDebug from "debug";
import loadConfig, { type ResolvedConfig } from "../../config";
import blockHoistPlugin from "../internal-plugins/block-hoist";
import shadowFunctionsPlugin from "../internal-plugins/shadow-functions";
const babelDebug = buildDebug("babel:file");
@@ -42,11 +41,11 @@ const errorVisitor = {
export default class File extends Store {
constructor({ options, passes }: ResolvedConfig) {
if (!INTERNAL_PLUGINS) {
// Lazy-init the internal plugins to remove the init-time circular dependency between plugins being
// Lazy-init the internal plugin to remove the init-time circular dependency between plugins being
// passed babel-core's export object, which loads this file, and this 'loadConfig' loading plugins.
INTERNAL_PLUGINS = loadConfig({
babelrc: false,
plugins: [ blockHoistPlugin, shadowFunctionsPlugin ],
plugins: [ blockHoistPlugin ],
}).passes[0];
}

View File

@@ -1,118 +0,0 @@
import * as t from "babel-types";
const SUPER_THIS_BOUND = Symbol("super this bound");
const superVisitor = {
CallExpression(path) {
if (!path.get("callee").isSuper()) return;
const { node } = path;
if (node[SUPER_THIS_BOUND]) return;
node[SUPER_THIS_BOUND] = true;
path.replaceWith(t.assignmentExpression("=", this.id, node));
},
};
export default {
name: "internal.shadowFunctions",
visitor: {
ThisExpression(path) {
remap(path, "this");
},
ReferencedIdentifier(path) {
if (path.node.name === "arguments") {
remap(path, "arguments");
}
},
},
};
function shouldShadow(path, shadowPath) {
if (path.is("_forceShadow")) {
return true;
} else {
return shadowPath;
}
}
function remap(path, key) {
// ensure that we're shadowed
const shadowPath = path.inShadow(key);
if (!shouldShadow(path, shadowPath)) return;
const shadowFunction = path.node._shadowedFunctionLiteral;
let currentFunction;
let passedShadowFunction = false;
let fnPath = path.find(function (innerPath) {
if (innerPath.parentPath && innerPath.parentPath.isClassProperty() && innerPath.key === "value") {
return true;
}
if (path === innerPath) return false;
if (innerPath.isProgram() || innerPath.isFunction()) {
// catch current function in case this is the shadowed one and we can ignore it
currentFunction = currentFunction || innerPath;
}
if (innerPath.isProgram()) {
passedShadowFunction = true;
return true;
} else if (innerPath.isFunction() && !innerPath.isArrowFunctionExpression()) {
if (shadowFunction) {
if (innerPath === shadowFunction || innerPath.node === shadowFunction.node) return true;
} else {
if (!innerPath.is("shadow")) return true;
}
passedShadowFunction = true;
return false;
}
return false;
});
if (shadowFunction && fnPath.isProgram() && !shadowFunction.isProgram()) {
// If the shadow wasn't found, take the closest function as a backup.
// This is a bit of a hack, but it will allow the parameter transforms to work properly
// without introducing yet another shadow-controlling flag.
fnPath = path.findParent((p) => p.isProgram() || p.isFunction());
}
// no point in realiasing if we're in this function
if (fnPath === currentFunction) return;
// If the only functions that were encountered are arrow functions, skip remapping the
// binding since arrow function syntax already does that.
if (!passedShadowFunction) return;
const cached = fnPath.getData(key);
if (cached) return path.replaceWith(cached);
const id = path.scope.generateUidIdentifier(key);
fnPath.setData(key, id);
const classPath = fnPath.findParent((p) => p.isClass());
const hasSuperClass = !!(classPath && classPath.node && classPath.node.superClass);
if (key === "this" && fnPath.isMethod({ kind: "constructor" }) && hasSuperClass) {
fnPath.scope.push({ id });
fnPath.traverse(superVisitor, { id });
} else {
const init = key === "this" ? t.thisExpression() : t.identifier(key);
// Forward the shadowed function, so that the identifiers do not get hoisted
// up to the first non shadow function but rather up to the bound shadow function
if (shadowFunction) init._shadowedFunctionLiteral = shadowFunction;
fnPath.scope.push({ id, init });
}
return path.replaceWith(id);
}

View File

@@ -27,10 +27,6 @@ const namedBuildWrapper = template(`
const awaitVisitor = {
Function(path) {
if (path.isArrowFunctionExpression() && !path.node.async) {
path.arrowFunctionToShadowed();
return;
}
path.skip();
},
@@ -84,7 +80,6 @@ function classOrObjectMethod(path: NodePath, callId: Object) {
node.async = false;
const container = t.functionExpression(null, [], t.blockStatement(body.body), true);
container.shadow = true;
body.body = [
t.returnStatement(t.callExpression(
t.callExpression(callId, [container]),
@@ -95,6 +90,9 @@ function classOrObjectMethod(path: NodePath, callId: Object) {
// Regardless of whether or not the wrapped function is a an async method
// or generator the outer function should not be
node.generator = false;
// Unwrap the wrapper IIFE's environment so super and this and such still work.
path.get("body.body.0.argument.callee.arguments.0").unwrapFunctionEnvironment();
}
function plainFunction(path: NodePath, callId: Object) {
@@ -104,7 +102,7 @@ function plainFunction(path: NodePath, callId: Object) {
let wrapper = buildWrapper;
if (path.isArrowFunctionExpression()) {
path.arrowFunctionToShadowed();
path.arrowFunctionToExpression();
} else if (!isDeclaration && asyncFnId) {
wrapper = namedBuildWrapper;
}
@@ -120,7 +118,7 @@ function plainFunction(path: NodePath, callId: Object) {
const built = t.callExpression(callId, [node]);
const container = wrapper({
NAME: asyncFnId,
NAME: asyncFnId || null,
REF: path.scope.generateUidIdentifier("ref"),
FUNCTION: built,
PARAMS: node.params.reduce((acc, param) => {

View File

@@ -46,15 +46,11 @@ function getPrototypeOfExpression(objectRef, isStatic) {
const visitor = {
Function(path) {
if (!path.inShadow("this")) {
path.skip();
}
if (!path.isArrowFunctionExpression()) path.skip();
},
ReturnStatement(path, state) {
if (!path.inShadow("this")) {
state.returns.push(path);
}
state.returns.push(path);
},
ThisExpression(path, state) {

View File

@@ -40,7 +40,7 @@ export const MESSAGES = {
pluginNotObject: "Plugin $2 specified in $1 was expected to return an object when invoked but returned $3",
pluginNotFunction: "Plugin $2 specified in $1 was expected to return a function but returned $3",
pluginUnknown: "Unknown plugin $1 specified in $2 at $3, attempted to resolve relative to $4",
pluginInvalidProperty: "Plugin $1 provided an invalid property of $3",
pluginInvalidProperty: "Plugin $1 provided an invalid property of $2",
};
/**

View File

@@ -2,9 +2,7 @@ let g = (() => {
var _ref = babelHelpers.asyncGenerator.wrap(function* () {
var _this = this;
(function () {
return _this;
});
() => this;
function f() {
() => this;
}

View File

@@ -11,11 +11,12 @@ let s = (() => {
var _ref2 = babelHelpers.asyncToGenerator(function* (y, a) {
let r = (() => {
var _ref3 = babelHelpers.asyncToGenerator(function* (z, b) {
yield z;
for (var _len2 = arguments.length, innerArgs = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
innerArgs[_key2 - 2] = arguments[_key2];
}
yield z;
console.log(_this, innerArgs, _arguments);
return _this.x;
});

View File

@@ -4,14 +4,10 @@ class Class {
return babelHelpers.asyncToGenerator(function* () {
_this;
(function () {
return _this;
});
(function () {
() => _this;
() => {
_this;
(function () {
return _this;
});
() => _this;
function x() {
var _this2 = this;
@@ -23,7 +19,7 @@ class Class {
_this2;
});
}
});
};
function x() {
var _this3 = this;

View File

@@ -0,0 +1,7 @@
class Foo extends class {} {
async method() {
super.method();
var arrow = () => super.method();
}
}

View File

@@ -0,0 +1,11 @@
class Foo extends class {} {
method() {
var _superprop_callMethod = (..._args) => super.method(..._args);
return babelHelpers.asyncToGenerator(function* () {
_superprop_callMethod();
var arrow = () => _superprop_callMethod();
})();
}
}

View File

@@ -0,0 +1,6 @@
{
"plugins": [
"transform-async-to-generator",
"external-helpers"
]
}

View File

@@ -1,6 +1,6 @@
let foo = (() => {
var _ref = _asyncToGenerator(function* () {
yield new _Promise(function (resolve) {
yield new _Promise(resolve => {
resolve();
});
});

View File

@@ -8,9 +8,6 @@
"keywords": [
"babel-plugin"
],
"dependencies": {
"babel-helper-function-name": "7.0.0-alpha.7"
},
"devDependencies": {
"babel-helper-plugin-test-runner": "7.0.0-alpha.9",
"babel-traverse": "7.0.0-alpha.9",

View File

@@ -1,43 +1,21 @@
// @flow
import nameFunction from "babel-helper-function-name";
import { type NodePath } from "babel-traverse";
import typeof * as babelTypes from "babel-types";
export default function ({ types: t }: { types: babelTypes }) {
export default function () {
return {
visitor: {
ArrowFunctionExpression(path: NodePath<BabelNodeArrowFunctionExpression>, state: Object) {
if (state.opts.spec) {
const { node } = path;
if (node.shadow) return;
// In some conversion cases, it may have already been converted to a function while this callback
// was queued up.
if (!path.isArrowFunctionExpression()) return;
node.shadow = { this: false };
node.type = "FunctionExpression";
const boundThis: any = t.thisExpression();
boundThis._forceShadow = path;
// make sure that arrow function won't be instantiated
path.ensureBlock();
path.get("body").unshiftContainer(
"body",
t.expressionStatement(t.callExpression(state.addHelper("newArrowCheck"), [
t.thisExpression(),
boundThis,
]))
);
const replacement = nameFunction(path);
const named = replacement || node;
path.replaceWith(t.callExpression(
t.memberExpression(named, t.identifier("bind")),
[t.thisExpression()]
));
} else {
path.arrowFunctionToShadowed();
}
path.arrowFunctionToExpression({
// While other utils may be fine inserting other arrows to make more transforms possible,
// the arrow transform itself absolutely cannot insert new arrow functions.
allowInsertArrow: false,
specCompliant: !!state.opts.spec,
});
},
},
};

View File

@@ -416,29 +416,19 @@ class BlockScoping {
// build the closure that we're going to wrap the block with, possible wrapping switch(){}
const fn = t.functionExpression(null, params,
t.blockStatement(isSwitch ? [block] : block.body));
fn.shadow = true;
// continuation
this.addContinuations(fn);
let ref = fn;
if (this.loop) {
ref = this.scope.generateUidIdentifier("loop");
this.loopPath.insertBefore(t.variableDeclaration("var", [
t.variableDeclarator(ref, fn),
]));
}
// build a call and a unique id that we can assign the return value to
let call = t.callExpression(ref, args);
const ret = this.scope.generateUidIdentifier("ret");
let call = t.callExpression(t.nullLiteral(), args);
let basePath = ".callee";
// handle generators
const hasYield = traverse.hasType(fn.body, this.scope, "YieldExpression", t.FUNCTION_TYPES);
if (hasYield) {
fn.generator = true;
call = t.yieldExpression(call, true);
basePath = ".argument" + basePath;
}
// handlers async functions
@@ -446,26 +436,57 @@ class BlockScoping {
if (hasAsync) {
fn.async = true;
call = t.awaitExpression(call);
basePath = ".argument" + basePath;
}
this.buildClosure(ret, call);
let placeholderPath;
let index;
if (this.has.hasReturn || this.has.hasBreakContinue) {
const ret = this.scope.generateUidIdentifier("ret");
// replace the current block body with the one we're going to build
if (isSwitch) this.blockPath.replaceWithMultiple(this.body);
else block.body = this.body;
}
this.body.push(t.variableDeclaration("var", [
t.variableDeclarator(ret, call),
]));
placeholderPath = "declarations.0.init" + basePath;
index = this.body.length - 1;
/**
* Push the closure to the body.
*/
buildClosure(ret: { type: "Identifier" }, call: { type: "CallExpression" }) {
const has = this.has;
if (has.hasReturn || has.hasBreakContinue) {
this.buildHas(ret, call);
this.buildHas(ret);
} else {
this.body.push(t.expressionStatement(call));
placeholderPath = "expression" + basePath;
index = this.body.length - 1;
}
let callPath;
// replace the current block body with the one we're going to build
if (isSwitch) {
const { parentPath, listKey, key } = this.blockPath;
this.blockPath.replaceWithMultiple(this.body);
callPath = parentPath.get(listKey)[key + index];
} else {
block.body = this.body;
callPath = this.blockPath.get("body")[index];
}
const placeholder = callPath.get(placeholderPath);
let fnPath;
if (this.loop) {
const ref = this.scope.generateUidIdentifier("loop");
const p = this.loopPath.insertBefore(t.variableDeclaration("var", [
t.variableDeclarator(ref, fn),
]));
placeholder.replaceWith(ref);
fnPath = p[0].get("declarations.0.init");
} else {
placeholder.replaceWith(fn);
fnPath = placeholder;
}
// Ensure "this", "arguments", and "super" continue to work in the wrapped function.
fnPath.unwrapFunctionEnvironment();
}
/**
@@ -643,13 +664,9 @@ class BlockScoping {
return replace;
}
buildHas(ret: { type: "Identifier" }, call: { type: "CallExpression" }) {
buildHas(ret: { type: "Identifier" }) {
const body = this.body;
body.push(t.variableDeclaration("var", [
t.variableDeclarator(ret, call),
]));
let retCheck;
const has = this.has;
const cases = [];

View File

@@ -1,5 +1,5 @@
(function () {
var _loop = function (i) {
var _loop2 = function (i) {
fns.push(function () {
return i;
});
@@ -14,18 +14,18 @@
}
};
_loop2: for (var i in nums) {
var _ret = _loop(i);
_loop: for (var i in nums) {
var _ret = _loop2(i);
switch (_ret) {
case "continue":
continue;
case "break":
break _loop2;
break _loop;
default:
if (typeof _ret === "object") return _ret.v;
}
}
})();
})();

View File

@@ -46,6 +46,10 @@ export default function ({ types: t }) {
if (state.opts.loose) Constructor = LooseTransformer;
path.replaceWith(new Constructor(path, state.file).run());
if (path.isCallExpression() && path.get("callee").isArrowFunctionExpression()) {
path.get("callee").arrowFunctionToExpression();
}
},
},
};

View File

@@ -14,9 +14,7 @@ const buildDerivedConstructor = template(`
const noMethodVisitor = {
"FunctionExpression|FunctionDeclaration"(path) {
if (!path.is("shadow")) {
path.skip();
}
path.skip();
},
Method(path) {
@@ -48,7 +46,9 @@ const verifyConstructorVisitor = visitors.merge([noMethodVisitor, {
ThisExpression(path) {
if (this.isDerived && !this.hasBareSuper) {
if (!path.inShadow("this")) {
const fn = path.find((p) => p.isFunction());
if (!fn || !fn.isArrowFunctionExpression()) {
throw path.buildCodeFrameError("'this' is not allowed before super()");
}
}
@@ -142,8 +142,7 @@ export default class ClassTransformer {
//
body.push(t.returnStatement(this.classRef));
const container = t.functionExpression(null, closureParams, t.blockStatement(body));
container.shadow = true;
const container = t.arrowFunctionExpression(closureParams, t.blockStatement(body));
return t.callExpression(container, closureArgs);
}

View File

@@ -139,8 +139,7 @@ export default function () {
if (
state.opts.allowTopLevelThis !== true &&
!path.findParent((path) => !path.is("shadow") &&
THIS_BREAK_KEYS.indexOf(path.type) >= 0)
!path.findParent((path) => THIS_BREAK_KEYS.indexOf(path.type) >= 0)
) {
path.replaceWith(t.identifier("undefined"));
}

View File

@@ -1,46 +1,39 @@
import ReplaceSupers from "babel-helper-replace-supers";
function replacePropertySuper(path, node, scope, getObjectRef, file) {
const replaceSupers = new ReplaceSupers({
getObjectRef: getObjectRef,
methodNode: node,
methodPath: path,
isStatic: true,
scope: scope,
file: file,
});
replaceSupers.replace();
}
export default function ({ types: t }) {
function Property(path, node, scope, getObjectRef, file) {
const replaceSupers = new ReplaceSupers({
getObjectRef: getObjectRef,
methodNode: node,
methodPath: path,
isStatic: true,
scope: scope,
file: file,
});
replaceSupers.replace();
}
const CONTAINS_SUPER = Symbol();
return {
visitor: {
Super(path) {
const parentObj = path.findParent((path) => path.isObjectExpression());
if (parentObj) parentObj.node[CONTAINS_SUPER] = true;
},
ObjectExpression(path, state) {
let objectRef;
const getObjectRef = () => objectRef = objectRef || path.scope.generateUidIdentifier("obj");
ObjectExpression: {
exit(path, file) {
if (!path.node[CONTAINS_SUPER]) return;
let objectRef;
const getObjectRef = () => objectRef = objectRef || path.scope.generateUidIdentifier("obj");
path.get("properties").forEach((propertyPath) => {
if (!propertyPath.isMethod()) return;
const propPaths: Array = path.get("properties");
for (let propPath of propPaths) {
if (propPath.isObjectProperty()) propPath = propPath.get("value");
Property(propPath, propPath.node, path.scope, getObjectRef, file);
replacePropertySuper(propPath, propPath.node, path.scope, getObjectRef, state);
}
});
if (objectRef) {
path.scope.push({ id: objectRef });
path.replaceWith(t.assignmentExpression("=", objectRef, path.node));
}
},
if (objectRef) {
path.scope.push({ id: objectRef });
path.replaceWith(t.assignmentExpression("=", objectRef, path.node));
}
},
},
};

View File

@@ -60,7 +60,6 @@ export const visitor = {
//
const argsIdentifier = t.identifier("arguments");
argsIdentifier._shadowedFunctionLiteral = path;
// push a default parameter definition
function pushDefNode(left, right, i) {

View File

@@ -9,11 +9,15 @@ export default function () {
return {
visitor: visitors.merge([{
ArrowFunctionExpression(path) {
// In some conversion cases, it may have already been converted to a function while this callback
// was queued up.
if (!path.isArrowFunctionExpression()) return;
// default/rest visitors require access to `arguments`
const params: Array<NodePath> = path.get("params");
for (const param of params) {
if (param.isRestElement() || param.isAssignmentPattern()) {
path.arrowFunctionToShadowed();
path.arrowFunctionToExpression();
break;
}
}

View File

@@ -208,9 +208,6 @@ export const visitor = {
const argsId = t.identifier("arguments");
// otherwise `arguments` will be remapped in arrow functions
argsId._shadowedFunctionLiteral = path;
// check and optimise for extremely common cases
const state = {
references: [],
@@ -263,9 +260,6 @@ export const visitor = {
state.candidates.map(({ path }) => path)
);
// deopt shadowed functions as transforms like regenerator may try touch the allocation loop
state.deopted = state.deopted || !!node.shadow;
const start = t.numericLiteral(node.params.length);
const key = scope.generateUidIdentifier("key");
const len = scope.generateUidIdentifier("len");

View File

@@ -30,11 +30,12 @@ function demo1() {
}
var x = function () {
if (noNeedToWork) return 0;
for (var _len2 = arguments.length, rest = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
rest[_key2] = arguments[_key2];
}
if (noNeedToWork) return 0;
return rest;
};

View File

@@ -11,11 +11,12 @@ var concat = function () {
var x = function () {
var _ref2 = babelHelpers.asyncToGenerator(function* () {
if (noNeedToWork) return 0;
for (var _len = arguments.length, rest = Array(_len), _key = 0; _key < _len; _key++) {
rest[_key] = arguments[_key];
}
if (noNeedToWork) return 0;
return rest;
});

View File

@@ -1,11 +1,13 @@
var _obj;
var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };
var _set = function set(object, property, value, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent !== null) { set(parent, property, value, receiver); } } else if ("value" in desc && desc.writable) { desc.value = value; } else { var setter = desc.set; if (setter !== undefined) { setter.call(receiver, value); } } return value; };
var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };
foo = _obj = {
bar() {
return _set(_obj.__proto__ || Object.getPrototypeOf(_obj), "baz", Math.pow(_get(_obj.__proto__ || Object.getPrototypeOf(_obj), "baz", this), 12), this);
var _ref;
return _ref = _get(_obj.__proto__ || Object.getPrototypeOf(_obj), "baz", this), _set(_obj.__proto__ || Object.getPrototypeOf(_obj), "baz", Math.pow(_ref, 12), this);
}
};
};

View File

@@ -1,15 +1,16 @@
"use strict";
var _this = undefined;
"1" + String(a);
(function () {
babelHelpers.newArrowCheck(undefined, undefined);
babelHelpers.newArrowCheck(this, _this);
}).bind(undefined);
function a() {
var _this = this;
var _this2 = this;
(function () {
babelHelpers.newArrowCheck(this, _this);
babelHelpers.newArrowCheck(this, _this2);
}).bind(this);
}

View File

@@ -9,6 +9,7 @@
"main": "lib/index.js",
"dependencies": {
"babel-code-frame": "7.0.0-alpha.9",
"babel-helper-function-name": "7.0.0-alpha.7",
"babel-messages": "7.0.0-alpha.9",
"babel-types": "7.0.0-alpha.9",
"babylon": "7.0.0-beta.8",

View File

@@ -200,59 +200,3 @@ export function inType() {
return false;
}
/**
* Checks whether the binding for 'key' is a local binding in its current function context.
*
* Checks if the current path either is, or has a direct parent function that is, inside
* of a function that is marked for shadowing of a binding matching 'key'. Also returns
* the parent path if the parent path is an arrow, since arrow functions pass through
* binding values to their parent, meaning they have no local bindings.
*
* Shadowing means that when the given binding is transformed, it will read the binding
* value from the container containing the shadow function, rather than from inside the
* shadow function.
*
* Function shadowing is acheieved by adding a "shadow" property on "FunctionExpression"
* and "FunctionDeclaration" node types.
*
* Node's "shadow" props have the following behavior:
*
* - Boolean true will cause the function to shadow both "this" and "arguments".
* - {this: false} Shadows "arguments" but not "this".
* - {arguments: false} Shadows "this" but not "arguments".
*
* Separately, individual identifiers can be flagged with two flags:
*
* - _forceShadow - If truthy, this specific identifier will be bound in the closest
* Function that is not flagged "shadow", or the Program.
* - _shadowedFunctionLiteral - When set to a NodePath, this specific identifier will be bound
* to this NodePath/Node or the Program. If this path is not found relative to the
* starting location path, the closest function will be used.
*
* Please Note, these flags are for private internal use only and should be avoided.
* Only "shadow" is a public property that other transforms may manipulate.
*/
export function inShadow(key?) {
const parentFn = this.isFunction() ? this : this.findParent((p) => p.isFunction());
if (!parentFn) return;
if (parentFn.isFunctionExpression() || parentFn.isFunctionDeclaration()) {
const shadow = parentFn.node.shadow;
// this is because sometimes we may have a `shadow` value of:
//
// { this: false }
//
// we need to catch this case if `inShadow` has been passed a `key`
if (shadow && (!key || shadow[key] !== false)) {
return parentFn;
}
} else if (parentFn.isArrowFunctionExpression()) {
return parentFn;
}
// normal function, we've found our function context
return null;
}

View File

@@ -1,6 +1,7 @@
// This file contains methods that convert the path node into another node or some other type of data.
import * as t from "babel-types";
import nameFunction from "babel-helper-function-name";
export function toComputedKey(): Object {
const node = this.node;
@@ -25,14 +26,433 @@ export function ensureBlock() {
return t.ensureBlock(this.node);
}
/**
* Keeping this for backward-compatibility. You should use arrowFunctionToExpression() for >=7.x.
*/
export function arrowFunctionToShadowed() {
// todo: maybe error
if (!this.isArrowFunctionExpression()) return;
this.ensureBlock();
const { node } = this;
node.expression = false;
node.type = "FunctionExpression";
node.shadow = node.shadow || true;
this.arrowFunctionToExpression();
}
/**
* Given an arbitrary function, process its content as if it were an arrow function, moving references
* to "this", "arguments", "super", and such into the function's parent scope. This method is useful if
* you have wrapped some set of items in an IIFE or other function, but want "this", "arguments", and super"
* to continue behaving as expected.
*/
export function unwrapFunctionEnvironment() {
if (!this.isArrowFunctionExpression() && !this.isFunctionExpression() && !this.isFunctionDeclaration()) {
throw this.buildCodeFrameError("Can only unwrap the environment of a function.");
}
hoistFunctionEnvironment(this);
}
/**
* Convert a given arrow function into a normal ES5 function expression.
*/
export function arrowFunctionToExpression({
allowInsertArrow = true,
specCompliant = false,
} = {}) {
if (!this.isArrowFunctionExpression()) {
throw this.buildCodeFrameError("Cannot convert non-arrow function to a function expression.");
}
const thisBinding = hoistFunctionEnvironment(this, specCompliant, allowInsertArrow);
this.ensureBlock();
this.node.type = "FunctionExpression";
if (specCompliant) {
const checkBinding = thisBinding ? null : this.parentPath.scope.generateUidIdentifier("arrowCheckId");
if (checkBinding) this.parentPath.scope.push({ id: checkBinding, init: t.objectExpression([]) });
this.get("body").unshiftContainer(
"body",
t.expressionStatement(t.callExpression(this.hub.file.addHelper("newArrowCheck"), [
t.thisExpression(),
checkBinding ? t.identifier(checkBinding.name) : t.identifier(thisBinding),
]))
);
this.replaceWith(
t.callExpression(
t.memberExpression(nameFunction(this) || this.node, t.identifier("bind")),
[checkBinding ? t.identifier(checkBinding.name) : t.thisExpression()]
)
);
}
}
/**
* 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.
*/
function hoistFunctionEnvironment(fnPath, specCompliant = false, allowInsertArrow = true) {
const thisEnvFn = fnPath.findParent(
(p) => (p.isFunction() && !p.isArrowFunctionExpression()) || p.isProgram() || p.isClassProperty());
const inConstructor = thisEnvFn && thisEnvFn.node.kind === "constructor";
if (thisEnvFn.isClassProperty()) {
throw fnPath.buildCodeFrameError("Unable to transform arrow inside class property");
}
const {
thisPaths,
argumentsPaths,
newTargetPaths,
superProps,
superCalls,
} = getScopeInformation(fnPath);
// Convert all super() calls in the constructor, if super is used in an arrow.
if (inConstructor && superCalls.length > 0) {
if (!allowInsertArrow) {
throw superCalls[0].buildCodeFrameError("Unable to handle nested super() usage in arrow");
}
const allSuperCalls = [];
thisEnvFn.traverse({
Function: (child) => {
if (child.isArrowFunctionExpression() || child.isClassProperty() || child === fnPath) return;
child.skip();
},
CallExpression(child) {
if (!child.get("callee").isSuper()) return;
allSuperCalls.push(child);
},
});
const superBinding = getSuperBinding(thisEnvFn);
allSuperCalls.forEach((superCall) => superCall.get("callee").replaceWith(t.identifier(superBinding)));
}
// Convert all "this" references in the arrow to point at the alias.
let thisBinding;
if (thisPaths.length > 0 || specCompliant) {
thisBinding = getThisBinding(thisEnvFn, inConstructor);
if (
!specCompliant ||
// In subclass constructors, still need to rewrite because "this" can't be bound in spec mode
// because it might not have been initialized yet.
(inConstructor && hasSuperClass(thisEnvFn))
) {
thisPaths.forEach((thisChild) => {
thisChild.replaceWith(thisChild.isJSX() ? t.jSXIdentifier(thisBinding) : t.identifier(thisBinding));
});
if (specCompliant) thisBinding = null;
}
}
// Convert all "arguments" references in the arrow to point at the alias.
if (argumentsPaths.length > 0) {
const argumentsBinding = getBinding(thisEnvFn, "arguments",
() => t.identifier("arguments"));
argumentsPaths.forEach((argumentsChild) => {
argumentsChild.replaceWith(t.identifier(argumentsBinding));
});
}
// Convert all "new.target" references in the arrow to point at the alias.
if (newTargetPaths.length > 0) {
const newTargetBinding = getBinding(thisEnvFn, "newtarget",
() => t.metaProperty(t.identifier("new"), t.identifier("target")));
newTargetPaths.forEach((argumentsChild) => {
argumentsChild.replaceWith(t.identifier(newTargetBinding));
});
}
// Convert all "super.prop" references to point at aliases.
if (superProps.length > 0) {
if (!allowInsertArrow) {
throw superProps[0].buildCodeFrameError("Unable to handle nested super.prop usage");
}
const flatSuperProps = superProps.reduce(
(acc, superProp) => acc.concat(standardizeSuperProperty(superProp)), []);
flatSuperProps.forEach((superProp) => {
const key = superProp.node.computed ? "" : superProp.get("property").node.name;
if (superProp.parentPath.isCallExpression({ callee: superProp.node })) {
const superBinding = getSuperPropCallBinding(thisEnvFn, key);
if (superProp.node.computed) {
const prop = superProp.get("property").node;
superProp.replaceWith(t.identifier(superBinding));
superProp.parentPath.node.arguments.unshift(prop);
} else {
superProp.replaceWith(t.identifier(superBinding));
}
} else {
const isAssignment = superProp.parentPath.isAssignmentExpression({ left: superProp.node });
const superBinding = getSuperPropBinding(thisEnvFn, isAssignment, key);
const args = [];
if (superProp.node.computed) {
args.push(superProp.get("property").node);
}
if (isAssignment) {
const value = superProp.parentPath.node.right;
args.push(value);
superProp.parentPath.replaceWith(t.callExpression(t.identifier(superBinding), args));
} else {
superProp.replaceWith(t.callExpression(t.identifier(superBinding), args));
}
}
});
}
return thisBinding;
}
function standardizeSuperProperty(superProp) {
if (superProp.parentPath.isAssignmentExpression() && superProp.parentPath.node.operator !== "=") {
const assignmentPath = superProp.parentPath;
const op = assignmentPath.node.operator.slice(0, -1);
const value = assignmentPath.node.right;
assignmentPath.node.operator = "=";
if (superProp.node.computed) {
const tmp = superProp.scope.generateDeclaredUidIdentifier("tmp");
assignmentPath.get("left").replaceWith(
t.memberExpression(superProp.node.object,
t.assignmentExpression("=", tmp, superProp.node.property), true /* computed */)
);
assignmentPath.get("right").replaceWith(
t.binaryExpression(op,
t.memberExpression(superProp.node.object, t.identifier(tmp.name), true /* computed */),
value,
)
);
} else {
assignmentPath.get("left").replaceWith(
t.memberExpression(superProp.node.object, superProp.node.property),
);
assignmentPath.get("right").replaceWith(
t.binaryExpression(op,
t.memberExpression(superProp.node.object, t.identifier(superProp.node.property.name)),
value,
)
);
}
return [ assignmentPath.get("left"), assignmentPath.get("right").get("left") ];
} else if (superProp.parentPath.isUpdateExpression()) {
const updateExpr = superProp.parentPath;
const tmp = superProp.scope.generateDeclaredUidIdentifier("tmp");
const computedKey = superProp.node.computed ?
superProp.scope.generateDeclaredUidIdentifier("prop") : null;
const parts = [
t.assignmentExpression(
"=",
tmp,
t.memberExpression(
superProp.node.object,
computedKey ?
t.assignmentExpression("=", computedKey, superProp.node.property) :
superProp.node.property,
superProp.node.computed,
),
),
t.assignmentExpression(
"=",
t.memberExpression(
superProp.node.object,
computedKey ? t.identifier(computedKey.name) : superProp.node.property,
superProp.node.computed,
),
t.binaryExpression("+", t.identifier(tmp.name), t.numericLiteral(1)),
),
];
if (!superProp.parentPath.node.prefix) {
parts.push(t.identifier(tmp.name));
}
updateExpr.replaceWith(t.sequenceExpression(parts));
const left = updateExpr.get("expressions.0.right");
const right = updateExpr.get("expressions.1.left");
return [ left, right ];
}
return [ superProp ];
}
function hasSuperClass(thisEnvFn) {
return thisEnvFn.isClassMethod() && !!thisEnvFn.parentPath.parentPath.node.superClass;
}
// Create a binding that evaluates to the "this" of the given function.
function getThisBinding(thisEnvFn, inConstructor) {
return getBinding(thisEnvFn, "this", (thisBinding) => {
if (!inConstructor || !hasSuperClass(thisEnvFn)) return t.thisExpression();
const supers = new WeakSet();
thisEnvFn.traverse({
Function: (child) => {
if (child.isArrowFunctionExpression() || child.isClassProperty() || child === this) return;
child.skip();
},
CallExpression(child) {
if (!child.get("callee").isSuper()) return;
if (supers.has(child.node)) return;
supers.add(child.node);
child.replaceWith(t.assignmentExpression("=", t.identifier(thisBinding), child.node));
},
});
});
}
// Create a binding for a function that will call "super()" with arguments passed through.
function getSuperBinding(thisEnvFn) {
return getBinding(thisEnvFn, "supercall", () => {
const argsBinding = thisEnvFn.scope.generateUidIdentifier("args");
return t.arrowFunctionExpression([t.restElement(argsBinding)], t.callExpression(t.super(), [
t.spreadElement(t.identifier(argsBinding.name)),
]));
});
}
// Create a binding for a function that will call "super.foo()" or "super[foo]()".
function getSuperPropCallBinding(thisEnvFn, propName) {
return getBinding(thisEnvFn, `superprop_call:${propName || ""}`, () => {
const argsBinding = thisEnvFn.scope.generateUidIdentifier("args");
const argsList = [ t.restElement(argsBinding) ];
let fnBody;
if (propName) {
// (...args) => super.foo(...args)
fnBody = t.callExpression(
t.memberExpression(t.super(), t.identifier(propName)),
[ t.spreadElement(t.identifier(argsBinding.name)) ],
);
} else {
const method = thisEnvFn.scope.generateUidIdentifier("prop");
// (method, ...args) => super[method](...args)
argsList.unshift(method);
fnBody = t.callExpression(
t.memberExpression(t.super(), t.identifier(method.name), true /* computed */),
[ t.spreadElement(t.identifier(argsBinding.name)) ],
);
}
return t.arrowFunctionExpression(argsList, fnBody);
});
}
// Create a binding for a function that will call "super.foo" or "super[foo]".
function getSuperPropBinding(thisEnvFn, isAssignment, propName) {
const op = isAssignment ? "set" : "get";
return getBinding(thisEnvFn, `superprop_${op}:${propName || ""}`, () => {
const argsList = [];
let fnBody;
if (propName) {
// () => super.foo
fnBody = t.memberExpression(t.super(), t.identifier(propName));
} else {
const method = thisEnvFn.scope.generateUidIdentifier("prop");
// (method) => super[method]
argsList.unshift(method);
fnBody = t.memberExpression(t.super(), t.identifier(method.name), true /* computed */);
}
if (isAssignment) {
const valueIdent = thisEnvFn.scope.generateUidIdentifier("value");
argsList.push(valueIdent);
fnBody = t.assignmentExpression("=", fnBody, t.identifier(valueIdent.name));
}
return t.arrowFunctionExpression(argsList, fnBody);
});
}
function getBinding(thisEnvFn, key, init) {
const cacheKey = "binding:" + key;
let data = thisEnvFn.getData(cacheKey);
if (!data) {
const id = thisEnvFn.scope.generateUidIdentifier(key);
data = id.name;
thisEnvFn.setData(cacheKey, data);
thisEnvFn.scope.push({
id: id,
init: init(data),
});
}
return data;
}
function getScopeInformation(fnPath) {
const thisPaths = [];
const argumentsPaths = [];
const newTargetPaths = [];
const superProps = [];
const superCalls = [];
fnPath.traverse({
Function(child) {
if (child.isArrowFunctionExpression() || child.isClassProperty()) return;
child.skip();
},
ThisExpression(child) {
thisPaths.push(child);
},
JSXIdentifier(child) {
if (child.node.name !== "this") return;
if (
!child.parentPath.isJSXMemberExpression({ object: child.node }) &&
!child.parentPath.isJSXOpeningElement({ name: child.node })
) return;
thisPaths.push(child);
},
CallExpression(child) {
if (child.get("callee").isSuper()) superCalls.push(child);
},
MemberExpression(child) {
if (child.get("object").isSuper()) superProps.push(child);
},
ReferencedIdentifier(child) {
if (child.node.name !== "arguments") return;
argumentsPaths.push(child);
},
MetaProperty(child) {
if (!child.get("meta").isIdentifier({ name: "new" })) return;
if (!child.get("property").isIdentifier({ name: "target" })) return;
newTargetPaths.push(child);
},
});
return {
thisPaths,
argumentsPaths,
newTargetPaths,
superProps,
superCalls,
};
}

View File

@@ -207,8 +207,7 @@ export function replaceExpressionWithStatements(nodes: Array<Object>) {
} else if (toSequenceExpression) {
this.replaceWith(toSequenceExpression);
} else {
const container = t.functionExpression(null, [], t.blockStatement(nodes));
container.shadow = true;
const container = t.arrowFunctionExpression([], t.blockStatement(nodes));
this.replaceWith(t.callExpression(container, []));
this.traverse(hoistVariablesVisitor);
@@ -239,6 +238,8 @@ export function replaceExpressionWithStatements(nodes: Array<Object>) {
}
}
this.get("callee").arrowFunctionToExpression();
return this.node;
}
}

View File

@@ -0,0 +1,498 @@
import { NodePath } from "../lib";
import assert from "assert";
import { parse } from "babylon";
import generate from "babel-generator";
import * as t from "babel-types";
function assertConversion(input, output, {
methodName = "method",
extend = false,
arrowOpts,
} = {}) {
const inputAst = wrapMethod(input, methodName, extend);
const outputAst = wrapMethod(output, methodName, extend);
const rootPath = NodePath.get({
hub: {
file: {
addHelper(helperName) {
return t.memberExpression(t.identifier("babelHelpers"), t.identifier(helperName));
},
},
},
parentPath: null,
parent: inputAst,
container: inputAst,
key: "program",
}).setContext();
rootPath.traverse({
ClassMethod(path) {
path.get("body.body.0.expression").arrowFunctionToExpression(arrowOpts);
},
});
assert.equal(generate(inputAst).code, generate(outputAst).code);
}
function wrapMethod(body, methodName, extend) {
return parse(`
class Example ${extend ? ("extends class {}") : ""} {
${methodName}() {${body} }
}
`, { plugins: ["jsx"] });
}
describe("arrow function conversion", () => {
it("should convert super calls in constructors", () => {
assertConversion(`
() => {
super(345);
};
super();
() => super();
`, `
var _supercall = (..._args) => super(..._args);
(function () {
_supercall(345);
});
_supercall();
() => _supercall();
`, { methodName: "constructor" });
});
it("should convert super calls and this references in constructors", () => {
assertConversion(`
() => {
super(345);
this;
};
super();
this;
() => super();
() => this;
`, `
var _supercall = (..._args) => _this = super(..._args),
_this;
(function () {
_supercall(345);
_this;
});
_supercall();
this;
() => _supercall();
() => this;
`, { methodName: "constructor", extend: true });
});
it("should convert this references in constructors", () => {
assertConversion(`
() => {
this;
};
super();
this;
() => super();
() => this;
`, `
var _this;
(function () {
_this;
});
_this = super();
this;
() => _this = super();
() => this;
`, { methodName: "constructor", extend: true });
});
it("should convert this references in constructors with spec compliance", () => {
assertConversion(`
() => {
this;
};
super();
this;
() => super();
() => this;
`, `
var _this,
_arrowCheckId = {};
(function () {
babelHelpers.newArrowCheck(this, _arrowCheckId);
_this;
}).bind(_arrowCheckId);
_this = super();
this;
() => _this = super();
() => this;
`, { methodName: "constructor", extend: true, arrowOpts: { specCompliant: true } });
});
it("should convert this references in constructors without extension", () => {
assertConversion(`
() => {
this;
};
this;
() => this;
`, `
var _this = this;
(function () {
_this;
});
this;
() => this;
`, { methodName: "constructor" });
});
it("should convert this references in constructors with spec compliance without extension", () => {
assertConversion(`
() => {
this;
};
this;
() => this;
`, `
var _this = this;
(function () {
babelHelpers.newArrowCheck(this, _this);
this;
}).bind(this);
this;
() => this;
`, { methodName: "constructor", arrowOpts: { specCompliant: true } });
});
it("should convert this references in methods", () => {
assertConversion(`
() => {
this;
};
this;
() => this;
`, `
var _this = this;
(function () {
_this;
});
this;
() => this;
`);
});
it("should convert this references in methods with spec compliance", () => {
assertConversion(`
() => {
this;
};
this;
() => this;
`, `
var _this = this;
(function () {
babelHelpers.newArrowCheck(this, _this);
this;
}).bind(this);
this;
() => this;
`, { arrowOpts: { specCompliant: true } });
});
it("should convert this references inside JSX in methods", () => {
assertConversion(`
() => {
<this.this this="" />;
};
<this.this this="" />;
() => <this.this this="" />;
`, `
var _this = this;
(function () {
<_this.this this="" />;
});
<this.this this="" />;
() => <this.this this="" />;
`);
});
it("should convert arguments references", () => {
assertConversion(`
() => {
arguments;
};
arguments;
() => arguments;
`, `
var _arguments = arguments;
(function () {
_arguments;
});
arguments;
() => arguments;
`);
});
it("should convert new.target references", () => {
assertConversion(`
() => {
new.target;
};
new.target;
() => new.target;
`, `
var _newtarget = new.target;
(function () {
_newtarget;
});
new.target;
() => new.target;
`);
});
it("should convert super.prop references", () => {
assertConversion(`
() => {
var tmp = super.foo;
};
super.foo;
() => super.foo;
`, `
var _superprop_getFoo = () => super.foo;
(function () {
var tmp = _superprop_getFoo();
});
super.foo;
() => super.foo;
`);
});
it("should convert super[prop] references", () => {
assertConversion(`
() => {
var tmp = super[foo];
};
super[foo];
() => super[foo];
`, `
var _superprop_get = _prop => super[_prop];
(function () {
var tmp = _superprop_get(foo);
});
super[foo];
() => super[foo];
`);
});
it("should convert super.prop assignment", () => {
assertConversion(`
() => {
super.foo = 4;
};
super.foo = 4;
() => super.foo = 4;
`, `
var _superprop_setFoo = _value => super.foo = _value;
(function () {
_superprop_setFoo(4);
});
super.foo = 4;
() => super.foo = 4;
`);
});
it("should convert super[prop] assignment", () => {
assertConversion(`
() => {
super[foo] = 4;
};
super[foo] = 4;
() => super[foo] = 4;
`, `
var _superprop_set = (_prop, _value) => super[_prop] = _value;
(function () {
_superprop_set(foo, 4);
});
super[foo] = 4;
() => super[foo] = 4;
`);
});
it("should convert super.prop operator assign", () => {
assertConversion(`
() => {
super.foo **= 4;
};
super.foo **= 4;
() => super.foo **= 4;
`, `
var _superprop_setFoo = _value => super.foo = _value,
_superprop_getFoo = () => super.foo;
(function () {
_superprop_setFoo(_superprop_getFoo() ** 4);
});
super.foo **= 4;
() => super.foo **= 4;
`);
});
it("should convert super[prop] operator assign", () => {
assertConversion(`
() => {
super[foo] **= 4;
};
super[foo] **= 4;
() => super[foo] **= 4;
`, `
var _superprop_set = (_prop, _value) => super[_prop] = _value,
_superprop_get = _prop2 => super[_prop2];
(function () {
var _tmp;
_superprop_set(_tmp = foo, _superprop_get(_tmp) ** 4);
});
super[foo] **= 4;
() => super[foo] **= 4;
`);
});
it("should convert super.prop prefix update", () => {
assertConversion(`
() => {
++super.foo;
};
++super.foo;
() => ++super.foo;
`, `
var _superprop_getFoo = () => super.foo,
_superprop_setFoo = _value => super.foo = _value;
(function () {
var _tmp;
_tmp = _superprop_getFoo(), _superprop_setFoo(_tmp + 1);
});
++super.foo;
() => ++super.foo;
`);
});
it("should convert super[prop] prefix update", () => {
assertConversion(`
() => {
++super[foo];
};
++super[foo];
() => ++super[foo];
`, `
var _superprop_get = _prop2 => super[_prop2],
_superprop_set = (_prop3, _value) => super[_prop3] = _value;
(function () {
var _tmp, _prop;
_tmp = _superprop_get(_prop = foo), _superprop_set(_prop, _tmp + 1);
});
++super[foo];
() => ++super[foo];
`);
});
it("should convert super.prop suffix update", () => {
assertConversion(`
() => {
super.foo++;
};
super.foo++;
() => super.foo++;
`, `
var _superprop_getFoo = () => super.foo,
_superprop_setFoo = _value => super.foo = _value;
(function () {
var _tmp;
_tmp = _superprop_getFoo(), _superprop_setFoo(_tmp + 1), _tmp;
});
super.foo++;
() => super.foo++;
`);
});
it("should convert super[prop] suffix update", () => {
assertConversion(`
() => {
super[foo]++;
};
super[foo]++;
() => super[foo]++;
`, `
var _superprop_get = _prop2 => super[_prop2],
_superprop_set = (_prop3, _value) => super[_prop3] = _value;
(function () {
var _tmp, _prop;
_tmp = _superprop_get(_prop = foo), _superprop_set(_prop, _tmp + 1), _tmp;
});
super[foo]++;
() => super[foo]++;
`);
});
it("should convert super.prop() calls", () => {
assertConversion(`
() => {
super.foo();
};
super.foo();
() => super.foo();
`, `
var _superprop_callFoo = (..._args) => super.foo(..._args);
(function () {
_superprop_callFoo();
});
super.foo();
() => super.foo();
`);
});
it("should convert super[prop]() calls", () => {
assertConversion(`
() => {
super[foo]();
};
super[foo]();
() => super[foo]();
`, `
var _superprop_call = (_prop, ..._args) => super[_prop](..._args);
(function () {
_superprop_call(foo);
});
super[foo]();
() => super[foo]();
`);
});
});

View File

@@ -245,10 +245,10 @@ defineType("MetaProperty", {
fields: {
// todo: limit to new.target
meta: {
validate: assertValueType("string"),
validate: assertNodeType("Identifier"),
},
property: {
validate: assertValueType("string"),
validate: assertNodeType("Identifier"),
},
},
});