From 14584c218cd8f61490c31d8bee3de34024fe32d1 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Fri, 5 May 2017 13:27:18 -0700 Subject: [PATCH] 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. --- .../src/transformation/file/index.js | 5 +- .../internal-plugins/shadow-functions.js | 118 ----- .../src/index.js | 12 +- .../babel-helper-replace-supers/src/index.js | 8 +- packages/babel-messages/src/index.js | 2 +- .../nested/arrows-in-declaration/expected.js | 4 +- .../deeply-nested-asyncs/expected.js | 3 +- .../object-method-with-arrows/expected.js | 12 +- .../object-method-with-super/actual.js | 7 + .../object-method-with-super/expected.js | 11 + .../object-method-with-super/options.json | 6 + .../shadowed-promise/expected.js | 2 +- .../package.json | 3 - .../src/index.js | 42 +- .../src/index.js | 79 +-- .../for-break-continue-return/expected.js | 10 +- .../src/index.js | 4 + .../src/vanilla.js | 11 +- .../src/index.js | 3 +- .../src/index.js | 55 +- .../src/default.js | 1 - .../src/index.js | 6 +- .../src/rest.js | 6 - .../rest-arrow-functions/expected.js | 3 +- .../rest-async-arrow-functions/expected.js | 3 +- .../test/fixtures/regression/4349/expected.js | 10 +- .../fixtures/preset-options/spec/expected.js | 7 +- packages/babel-traverse/package.json | 1 + packages/babel-traverse/src/path/ancestry.js | 56 -- .../babel-traverse/src/path/conversion.js | 434 ++++++++++++++- .../babel-traverse/src/path/replacement.js | 5 +- .../babel-traverse/test/arrow-transform.js | 498 ++++++++++++++++++ .../babel-types/src/definitions/es2015.js | 4 +- 33 files changed, 1089 insertions(+), 342 deletions(-) delete mode 100644 packages/babel-core/src/transformation/internal-plugins/shadow-functions.js create mode 100644 packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/actual.js create mode 100644 packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/expected.js create mode 100644 packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/options.json create mode 100644 packages/babel-traverse/test/arrow-transform.js diff --git a/packages/babel-core/src/transformation/file/index.js b/packages/babel-core/src/transformation/file/index.js index 9cba7b326c..52b8a35d81 100644 --- a/packages/babel-core/src/transformation/file/index.js +++ b/packages/babel-core/src/transformation/file/index.js @@ -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]; } diff --git a/packages/babel-core/src/transformation/internal-plugins/shadow-functions.js b/packages/babel-core/src/transformation/internal-plugins/shadow-functions.js deleted file mode 100644 index 45122a0a3f..0000000000 --- a/packages/babel-core/src/transformation/internal-plugins/shadow-functions.js +++ /dev/null @@ -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); -} diff --git a/packages/babel-helper-remap-async-to-generator/src/index.js b/packages/babel-helper-remap-async-to-generator/src/index.js index 612fe18fbf..04097c7f8e 100644 --- a/packages/babel-helper-remap-async-to-generator/src/index.js +++ b/packages/babel-helper-remap-async-to-generator/src/index.js @@ -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) => { diff --git a/packages/babel-helper-replace-supers/src/index.js b/packages/babel-helper-replace-supers/src/index.js index 47c47522d8..bc17d07390 100644 --- a/packages/babel-helper-replace-supers/src/index.js +++ b/packages/babel-helper-replace-supers/src/index.js @@ -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) { diff --git a/packages/babel-messages/src/index.js b/packages/babel-messages/src/index.js index 53b559d437..088e586b09 100644 --- a/packages/babel-messages/src/index.js +++ b/packages/babel-messages/src/index.js @@ -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", }; /** diff --git a/packages/babel-plugin-transform-async-generator-functions/test/fixtures/nested/arrows-in-declaration/expected.js b/packages/babel-plugin-transform-async-generator-functions/test/fixtures/nested/arrows-in-declaration/expected.js index 23d3264481..aba2c275d1 100644 --- a/packages/babel-plugin-transform-async-generator-functions/test/fixtures/nested/arrows-in-declaration/expected.js +++ b/packages/babel-plugin-transform-async-generator-functions/test/fixtures/nested/arrows-in-declaration/expected.js @@ -2,9 +2,7 @@ let g = (() => { var _ref = babelHelpers.asyncGenerator.wrap(function* () { var _this = this; - (function () { - return _this; - }); + () => this; function f() { () => this; } diff --git a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/deeply-nested-asyncs/expected.js b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/deeply-nested-asyncs/expected.js index 28d7f36ebb..82882196f9 100644 --- a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/deeply-nested-asyncs/expected.js +++ b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/deeply-nested-asyncs/expected.js @@ -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; }); diff --git a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-arrows/expected.js b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-arrows/expected.js index 60c0717eb5..fe4b2f89a7 100644 --- a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-arrows/expected.js +++ b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-arrows/expected.js @@ -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; diff --git a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/actual.js b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/actual.js new file mode 100644 index 0000000000..6e32273d67 --- /dev/null +++ b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/actual.js @@ -0,0 +1,7 @@ +class Foo extends class {} { + async method() { + super.method(); + + var arrow = () => super.method(); + } +} diff --git a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/expected.js b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/expected.js new file mode 100644 index 0000000000..30b75ca0e2 --- /dev/null +++ b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/expected.js @@ -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(); + })(); + } +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/options.json b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/options.json new file mode 100644 index 0000000000..791b275929 --- /dev/null +++ b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/object-method-with-super/options.json @@ -0,0 +1,6 @@ +{ + "plugins": [ + "transform-async-to-generator", + "external-helpers" + ] +} diff --git a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/shadowed-promise/expected.js b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/shadowed-promise/expected.js index b759133cbe..5687661a35 100644 --- a/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/shadowed-promise/expected.js +++ b/packages/babel-plugin-transform-async-to-generator/test/fixtures/async-to-generator/shadowed-promise/expected.js @@ -1,6 +1,6 @@ let foo = (() => { var _ref = _asyncToGenerator(function* () { - yield new _Promise(function (resolve) { + yield new _Promise(resolve => { resolve(); }); }); diff --git a/packages/babel-plugin-transform-es2015-arrow-functions/package.json b/packages/babel-plugin-transform-es2015-arrow-functions/package.json index cfbb6983e6..74976e115b 100644 --- a/packages/babel-plugin-transform-es2015-arrow-functions/package.json +++ b/packages/babel-plugin-transform-es2015-arrow-functions/package.json @@ -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", diff --git a/packages/babel-plugin-transform-es2015-arrow-functions/src/index.js b/packages/babel-plugin-transform-es2015-arrow-functions/src/index.js index c3971f5d98..dd6dcd6059 100644 --- a/packages/babel-plugin-transform-es2015-arrow-functions/src/index.js +++ b/packages/babel-plugin-transform-es2015-arrow-functions/src/index.js @@ -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, 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, + }); }, }, }; diff --git a/packages/babel-plugin-transform-es2015-block-scoping/src/index.js b/packages/babel-plugin-transform-es2015-block-scoping/src/index.js index 1942cb8846..3916769c47 100644 --- a/packages/babel-plugin-transform-es2015-block-scoping/src/index.js +++ b/packages/babel-plugin-transform-es2015-block-scoping/src/index.js @@ -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 = []; diff --git a/packages/babel-plugin-transform-es2015-block-scoping/test/fixtures/general/for-break-continue-return/expected.js b/packages/babel-plugin-transform-es2015-block-scoping/test/fixtures/general/for-break-continue-return/expected.js index 3135af5869..f8106132ae 100644 --- a/packages/babel-plugin-transform-es2015-block-scoping/test/fixtures/general/for-break-continue-return/expected.js +++ b/packages/babel-plugin-transform-es2015-block-scoping/test/fixtures/general/for-break-continue-return/expected.js @@ -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; } } -})(); +})(); \ No newline at end of file diff --git a/packages/babel-plugin-transform-es2015-classes/src/index.js b/packages/babel-plugin-transform-es2015-classes/src/index.js index 1e41a94f5a..8089594177 100644 --- a/packages/babel-plugin-transform-es2015-classes/src/index.js +++ b/packages/babel-plugin-transform-es2015-classes/src/index.js @@ -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(); + } }, }, }; diff --git a/packages/babel-plugin-transform-es2015-classes/src/vanilla.js b/packages/babel-plugin-transform-es2015-classes/src/vanilla.js index cdf20beb75..3315509c4e 100644 --- a/packages/babel-plugin-transform-es2015-classes/src/vanilla.js +++ b/packages/babel-plugin-transform-es2015-classes/src/vanilla.js @@ -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); } diff --git a/packages/babel-plugin-transform-es2015-modules-commonjs/src/index.js b/packages/babel-plugin-transform-es2015-modules-commonjs/src/index.js index a9178da61e..c5832e35b7 100644 --- a/packages/babel-plugin-transform-es2015-modules-commonjs/src/index.js +++ b/packages/babel-plugin-transform-es2015-modules-commonjs/src/index.js @@ -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")); } diff --git a/packages/babel-plugin-transform-es2015-object-super/src/index.js b/packages/babel-plugin-transform-es2015-object-super/src/index.js index dd47816a7d..71ddc26737 100644 --- a/packages/babel-plugin-transform-es2015-object-super/src/index.js +++ b/packages/babel-plugin-transform-es2015-object-super/src/index.js @@ -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)); + } }, }, }; diff --git a/packages/babel-plugin-transform-es2015-parameters/src/default.js b/packages/babel-plugin-transform-es2015-parameters/src/default.js index 6b0d728f5e..49d6999bb9 100644 --- a/packages/babel-plugin-transform-es2015-parameters/src/default.js +++ b/packages/babel-plugin-transform-es2015-parameters/src/default.js @@ -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) { diff --git a/packages/babel-plugin-transform-es2015-parameters/src/index.js b/packages/babel-plugin-transform-es2015-parameters/src/index.js index c3321d35c3..673fbc5359 100644 --- a/packages/babel-plugin-transform-es2015-parameters/src/index.js +++ b/packages/babel-plugin-transform-es2015-parameters/src/index.js @@ -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 = path.get("params"); for (const param of params) { if (param.isRestElement() || param.isAssignmentPattern()) { - path.arrowFunctionToShadowed(); + path.arrowFunctionToExpression(); break; } } diff --git a/packages/babel-plugin-transform-es2015-parameters/src/rest.js b/packages/babel-plugin-transform-es2015-parameters/src/rest.js index f0171ed443..820fd7b247 100644 --- a/packages/babel-plugin-transform-es2015-parameters/src/rest.js +++ b/packages/babel-plugin-transform-es2015-parameters/src/rest.js @@ -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"); diff --git a/packages/babel-plugin-transform-es2015-parameters/test/fixtures/parameters/rest-arrow-functions/expected.js b/packages/babel-plugin-transform-es2015-parameters/test/fixtures/parameters/rest-arrow-functions/expected.js index 83e4df00b8..68017e2d86 100644 --- a/packages/babel-plugin-transform-es2015-parameters/test/fixtures/parameters/rest-arrow-functions/expected.js +++ b/packages/babel-plugin-transform-es2015-parameters/test/fixtures/parameters/rest-arrow-functions/expected.js @@ -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; }; diff --git a/packages/babel-plugin-transform-es2015-parameters/test/fixtures/parameters/rest-async-arrow-functions/expected.js b/packages/babel-plugin-transform-es2015-parameters/test/fixtures/parameters/rest-async-arrow-functions/expected.js index 3cad4fb5af..ce70a1f3ba 100644 --- a/packages/babel-plugin-transform-es2015-parameters/test/fixtures/parameters/rest-async-arrow-functions/expected.js +++ b/packages/babel-plugin-transform-es2015-parameters/test/fixtures/parameters/rest-async-arrow-functions/expected.js @@ -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; }); diff --git a/packages/babel-plugin-transform-exponentiation-operator/test/fixtures/regression/4349/expected.js b/packages/babel-plugin-transform-exponentiation-operator/test/fixtures/regression/4349/expected.js index 980008bd09..8aa531ebb7 100644 --- a/packages/babel-plugin-transform-exponentiation-operator/test/fixtures/regression/4349/expected.js +++ b/packages/babel-plugin-transform-exponentiation-operator/test/fixtures/regression/4349/expected.js @@ -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); } -}; +}; \ No newline at end of file diff --git a/packages/babel-preset-es2015/test/fixtures/preset-options/spec/expected.js b/packages/babel-preset-es2015/test/fixtures/preset-options/spec/expected.js index 971ccffce5..1ff90ab1ed 100644 --- a/packages/babel-preset-es2015/test/fixtures/preset-options/spec/expected.js +++ b/packages/babel-preset-es2015/test/fixtures/preset-options/spec/expected.js @@ -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); } diff --git a/packages/babel-traverse/package.json b/packages/babel-traverse/package.json index 5219834a6b..cd777cd3ed 100644 --- a/packages/babel-traverse/package.json +++ b/packages/babel-traverse/package.json @@ -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", diff --git a/packages/babel-traverse/src/path/ancestry.js b/packages/babel-traverse/src/path/ancestry.js index 245910aa6a..3aa106ed9b 100644 --- a/packages/babel-traverse/src/path/ancestry.js +++ b/packages/babel-traverse/src/path/ancestry.js @@ -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; -} diff --git a/packages/babel-traverse/src/path/conversion.js b/packages/babel-traverse/src/path/conversion.js index a9ff22fc64..87d7e5490e 100644 --- a/packages/babel-traverse/src/path/conversion.js +++ b/packages/babel-traverse/src/path/conversion.js @@ -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, + }; } diff --git a/packages/babel-traverse/src/path/replacement.js b/packages/babel-traverse/src/path/replacement.js index e82bba5b39..b021f6c895 100644 --- a/packages/babel-traverse/src/path/replacement.js +++ b/packages/babel-traverse/src/path/replacement.js @@ -207,8 +207,7 @@ export function replaceExpressionWithStatements(nodes: Array) { } 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) { } } + this.get("callee").arrowFunctionToExpression(); + return this.node; } } diff --git a/packages/babel-traverse/test/arrow-transform.js b/packages/babel-traverse/test/arrow-transform.js new file mode 100644 index 0000000000..3d883d5000 --- /dev/null +++ b/packages/babel-traverse/test/arrow-transform.js @@ -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(` + () => { + ; + }; + ; + () => ; + `, ` + var _this = this; + + (function () { + <_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](); + `); + }); +}); diff --git a/packages/babel-types/src/definitions/es2015.js b/packages/babel-types/src/definitions/es2015.js index 450ce3d40b..7cb893384c 100644 --- a/packages/babel-types/src/definitions/es2015.js +++ b/packages/babel-types/src/definitions/es2015.js @@ -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"), }, }, });