From c2a42492db7ad7c5f148bae6be6896916a2d7686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=C3=A1ng=20J=C3=B9nli=C3=A0ng?= Date: Fri, 19 Mar 2021 13:26:28 -0400 Subject: [PATCH] Implement @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining (#13009) --- Gulpfile.mjs | 1 + lib/babel-packages.js.flow | 15 +- .../.npmignore | 3 + .../README.md | 19 ++ .../package.json | 38 +++ .../src/index.ts | 22 ++ .../src/util.ts | 59 +++++ .../assumption-noDocumentAll/basic/input.mjs | 7 + .../assumption-noDocumentAll/basic/output.mjs | 6 + .../function-call/input.js | 19 ++ .../function-call/output.js | 12 + .../input.mjs | 5 + .../options.json | 5 + .../output.mjs | 5 + .../test/fixtures/basic/basic/input.mjs | 7 + .../test/fixtures/basic/basic/output.mjs | 6 + .../exec.mjs | 13 + .../input.mjs | 9 + .../options.json | 5 + .../output.mjs | 18 ++ .../basic/class-private-integration/exec.mjs | 13 + .../basic/class-private-integration/input.mjs | 9 + .../class-private-integration/options.json | 5 + .../class-private-integration/output.mjs | 18 ++ .../fixtures/basic/class-private/exec.mjs | 13 + .../fixtures/basic/class-private/input.mjs | 9 + .../fixtures/basic/class-private/options.json | 10 + .../fixtures/basic/class-private/output.mjs | 13 + .../test/fixtures/options.json | 3 + .../test/index.js | 3 + .../test/util.test.js | 115 +++++++++ .../src/index.js | 228 +----------------- .../src/transform.js | 219 +++++++++++++++++ packages/babel-preset-env/package.json | 1 + .../babel-preset-env/src/available-plugins.js | 2 + .../input.js | 3 + .../options.json | 12 + .../output.js | 4 + .../stdout.txt | 22 ++ .../input.js | 3 + .../options.json | 12 + .../output.js | 4 + .../stdout.txt | 23 ++ yarn.lock | 16 ++ 44 files changed, 807 insertions(+), 227 deletions(-) create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/.npmignore create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/README.md create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/package.json create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/index.ts create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/util.ts create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-noDocumentAll/basic/input.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-noDocumentAll/basic/output.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-pureGetters/function-call/input.js create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-pureGetters/function-call/output.js create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/input.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/options.json create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/output.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic/input.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic/output.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/exec.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/input.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/options.json create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/output.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/exec.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/input.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/options.json create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/output.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/exec.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/input.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/options.json create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/output.mjs create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/options.json create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/index.js create mode 100644 packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/util.test.js create mode 100644 packages/babel-plugin-proposal-optional-chaining/src/transform.js create mode 100644 packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/input.js create mode 100644 packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/options.json create mode 100644 packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/output.js create mode 100644 packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/stdout.txt create mode 100644 packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/input.js create mode 100644 packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/options.json create mode 100644 packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/output.js create mode 100644 packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/stdout.txt diff --git a/Gulpfile.mjs b/Gulpfile.mjs index a9b0ccbe7511..21fdd85839a3 100644 --- a/Gulpfile.mjs +++ b/Gulpfile.mjs @@ -426,6 +426,7 @@ const libBundles = [ "packages/babel-plugin-proposal-optional-chaining", "packages/babel-preset-typescript", "packages/babel-helper-member-expression-to-functions", + "packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining", ].map(src => ({ src, format: "cjs", diff --git a/lib/babel-packages.js.flow b/lib/babel-packages.js.flow index fc29e08ae4b5..48f2bdc5ee8c 100644 --- a/lib/babel-packages.js.flow +++ b/lib/babel-packages.js.flow @@ -166,7 +166,9 @@ declare module "@babel/helper-function-name" { } declare module "@babel/helper-split-export-declaration" { - declare export default function splitExportDeclaration(exportDeclaration: any): any; + declare export default function splitExportDeclaration( + exportDeclaration: any + ): any; } declare module "@babel/traverse" { @@ -196,6 +198,13 @@ declare module "@babel/highlight" { /** * Highlight `code`. */ - declare export default function highlight(code: string, options?: Options): string; - declare export { getChalk, shouldHighlight }; + declare export default function highlight( + code: string, + options?: Options + ): string; + declare export { getChalk, shouldHighlight }; +} + +declare module "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" { + declare module.exports: any; } diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/.npmignore b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/.npmignore new file mode 100644 index 000000000000..f9806945836e --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/.npmignore @@ -0,0 +1,3 @@ +src +test +*.log diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/README.md b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/README.md new file mode 100644 index 000000000000..aeca24cd090f --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/README.md @@ -0,0 +1,19 @@ +# @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining + +> Transform optional chaining operators to workaround a [v8 bug](https://crbug.com/v8/11558). + +See our website [@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining](https://babeljs.io/docs/en/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining) for more information. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining +``` + +or using yarn: + +```sh +yarn add @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining --dev +``` diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/package.json b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/package.json new file mode 100644 index 000000000000..01a1d545fd33 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/package.json @@ -0,0 +1,38 @@ +{ + "name": "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining", + "version": "7.13.8", + "description": "Transform optional chaining operators to workaround https://crbug.com/v8/11558", + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining" + }, + "homepage": "https://babel.dev/docs/en/next/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "lib/index.js", + "exports": { + ".": [ + "./lib/index.js" + ] + }, + "keywords": [ + "babel-plugin", + "bugfix" + ], + "dependencies": { + "@babel/helper-plugin-utils": "workspace:^7.13.0", + "@babel/helper-skip-transparent-expression-wrappers": "workspace:^7.12.1", + "@babel/plugin-proposal-optional-chaining": "workspace:^7.13.8" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + }, + "devDependencies": { + "@babel/core": "workspace:*", + "@babel/helper-plugin-test-runner": "workspace:*", + "@babel/traverse": "workspace:*" + } +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/index.ts b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/index.ts new file mode 100644 index 000000000000..23a3299ea652 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/index.ts @@ -0,0 +1,22 @@ +import { declare } from "@babel/helper-plugin-utils"; +import { transform } from "@babel/plugin-proposal-optional-chaining"; +import { shouldTransform } from "./util"; + +export default declare(api => { + api.assertVersion(7); + + const noDocumentAll = api.assumption("noDocumentAll"); + const pureGetters = api.assumption("pureGetters"); + + return { + name: "bugfix-v8-spread-parameters-in-optional-chaining", + + visitor: { + "OptionalCallExpression|OptionalMemberExpression"(path) { + if (shouldTransform(path)) { + transform(path, { noDocumentAll, pureGetters }); + } + }, + }, + }; +}); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/util.ts b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/util.ts new file mode 100644 index 000000000000..ebfdc8bebe55 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/util.ts @@ -0,0 +1,59 @@ +import { skipTransparentExprWrappers } from "@babel/helper-skip-transparent-expression-wrappers"; +import type { NodePath } from "@babel/traverse"; +import { types as t } from "@babel/core"; +// https://crbug.com/v8/11558 + +// check if there is a spread element followed by another argument. +// (...[], 0) or (...[], ...[]) + +function matchAffectedArguments(argumentNodes) { + const spreadIndex = argumentNodes.findIndex(node => t.isSpreadElement(node)); + return spreadIndex >= 0 && spreadIndex !== argumentNodes.length - 1; +} + +/** + * Check whether the optional chain is affected by https://crbug.com/v8/11558. + * This routine MUST not manipulate NodePath + * + * @export + * @param {(NodePath)} path + * @returns {boolean} + */ +export function shouldTransform( + path: NodePath, +): boolean { + let optionalPath = path; + const chains = []; + while ( + optionalPath.isOptionalMemberExpression() || + optionalPath.isOptionalCallExpression() + ) { + const { node } = optionalPath; + chains.push(node); + + if (optionalPath.isOptionalMemberExpression()) { + optionalPath = skipTransparentExprWrappers(optionalPath.get("object")); + } else if (optionalPath.isOptionalCallExpression()) { + optionalPath = skipTransparentExprWrappers(optionalPath.get("callee")); + } + } + for (let i = 0; i < chains.length; i++) { + const node = chains[i]; + if ( + t.isOptionalCallExpression(node) && + matchAffectedArguments(node.arguments) + ) { + // f?.(...[], 0) + if (node.optional) { + return true; + } + // o?.m(...[], 0) + // when node.optional is false, chains[i + 1] is always well defined + const callee = chains[i + 1]; + if (t.isOptionalMemberExpression(callee, { optional: true })) { + return true; + } + } + } + return false; +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-noDocumentAll/basic/input.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-noDocumentAll/basic/input.mjs new file mode 100644 index 000000000000..fda29744b178 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-noDocumentAll/basic/input.mjs @@ -0,0 +1,7 @@ +fn?.(...b, 1); + +a?.b(...c, 1); + +a?.b?.(...c, 1); + +a.b?.(...c, 1); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-noDocumentAll/basic/output.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-noDocumentAll/basic/output.mjs new file mode 100644 index 000000000000..3f4e23a9738d --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-noDocumentAll/basic/output.mjs @@ -0,0 +1,6 @@ +var _fn, _a, _a2, _a2$b, _a$b, _a3; + +(_fn = fn) === null || _fn === void 0 ? void 0 : _fn(...b, 1); +(_a = a) === null || _a === void 0 ? void 0 : _a.b(...c, 1); +(_a2 = a) === null || _a2 === void 0 ? void 0 : (_a2$b = _a2.b) === null || _a2$b === void 0 ? void 0 : _a2$b.call(_a2, ...c, 1); +(_a$b = (_a3 = a).b) === null || _a$b === void 0 ? void 0 : _a$b.call(_a3, ...c, 1); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-pureGetters/function-call/input.js b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-pureGetters/function-call/input.js new file mode 100644 index 000000000000..d3b392aabeb8 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-pureGetters/function-call/input.js @@ -0,0 +1,19 @@ +foo?.(...[], 1); + +foo?.bar(...[], 1) + +foo.bar?.(foo.bar, ...[], 1) + +foo?.bar?.(foo.bar, ...[], 1) + +foo?.(...[], 1).bar + +foo?.(...[], 1)?.bar + +foo.bar?.(...[], 1).baz + +foo.bar?.(...[], 1)?.baz + +foo?.bar?.(...[], 1).baz + +foo?.bar?.(...[], 1)?.baz diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-pureGetters/function-call/output.js b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-pureGetters/function-call/output.js new file mode 100644 index 000000000000..96e701785bc1 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/assumption-pureGetters/function-call/output.js @@ -0,0 +1,12 @@ +var _foo, _foo2, _foo$bar, _foo3, _foo4, _foo4$bar, _foo5, _foo6, _foo7, _foo$bar2, _foo8, _foo$bar3, _foo9, _foo$bar3$call, _foo10, _foo10$bar, _foo11, _foo11$bar, _foo11$bar$call; + +(_foo = foo) === null || _foo === void 0 ? void 0 : _foo(...[], 1); +(_foo2 = foo) === null || _foo2 === void 0 ? void 0 : _foo2.bar(...[], 1); +(_foo$bar = (_foo3 = foo).bar) === null || _foo$bar === void 0 ? void 0 : _foo$bar.call(_foo3, foo.bar, ...[], 1); +(_foo4 = foo) === null || _foo4 === void 0 ? void 0 : (_foo4$bar = _foo4.bar) === null || _foo4$bar === void 0 ? void 0 : _foo4$bar.call(_foo4, foo.bar, ...[], 1); +(_foo5 = foo) === null || _foo5 === void 0 ? void 0 : _foo5(...[], 1).bar; +(_foo6 = foo) === null || _foo6 === void 0 ? void 0 : (_foo7 = _foo6(...[], 1)) === null || _foo7 === void 0 ? void 0 : _foo7.bar; +(_foo$bar2 = (_foo8 = foo).bar) === null || _foo$bar2 === void 0 ? void 0 : _foo$bar2.call(_foo8, ...[], 1).baz; +(_foo$bar3 = (_foo9 = foo).bar) === null || _foo$bar3 === void 0 ? void 0 : (_foo$bar3$call = _foo$bar3.call(_foo9, ...[], 1)) === null || _foo$bar3$call === void 0 ? void 0 : _foo$bar3$call.baz; +(_foo10 = foo) === null || _foo10 === void 0 ? void 0 : (_foo10$bar = _foo10.bar) === null || _foo10$bar === void 0 ? void 0 : _foo10$bar.call(_foo10, ...[], 1).baz; +(_foo11 = foo) === null || _foo11 === void 0 ? void 0 : (_foo11$bar = _foo11.bar) === null || _foo11$bar === void 0 ? void 0 : (_foo11$bar$call = _foo11$bar.call(_foo11, ...[], 1)) === null || _foo11$bar$call === void 0 ? void 0 : _foo11$bar$call.baz; diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/input.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/input.mjs new file mode 100644 index 000000000000..856341a4f751 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/input.mjs @@ -0,0 +1,5 @@ +fn?.(...b, 1); + +a?.b(...c, 1); + +a?.b?.(...c, 1); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/options.json b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/options.json new file mode 100644 index 000000000000..5f731c2eb9fb --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "proposal-optional-chaining", "bugfix-v8-spread-parameters-in-optional-chaining" + ] +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/output.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/output.mjs new file mode 100644 index 000000000000..b92d011412f6 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic-integration-optional-chaining/output.mjs @@ -0,0 +1,5 @@ +var _fn, _a, _a2, _a2$b; + +(_fn = fn) === null || _fn === void 0 ? void 0 : _fn(...b, 1); +(_a = a) === null || _a === void 0 ? void 0 : _a.b(...c, 1); +(_a2 = a) === null || _a2 === void 0 ? void 0 : (_a2$b = _a2.b) === null || _a2$b === void 0 ? void 0 : _a2$b.call(_a2, ...c, 1); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic/input.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic/input.mjs new file mode 100644 index 000000000000..fda29744b178 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic/input.mjs @@ -0,0 +1,7 @@ +fn?.(...b, 1); + +a?.b(...c, 1); + +a?.b?.(...c, 1); + +a.b?.(...c, 1); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic/output.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic/output.mjs new file mode 100644 index 000000000000..3f4e23a9738d --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/basic/output.mjs @@ -0,0 +1,6 @@ +var _fn, _a, _a2, _a2$b, _a$b, _a3; + +(_fn = fn) === null || _fn === void 0 ? void 0 : _fn(...b, 1); +(_a = a) === null || _a === void 0 ? void 0 : _a.b(...c, 1); +(_a2 = a) === null || _a2 === void 0 ? void 0 : (_a2$b = _a2.b) === null || _a2$b === void 0 ? void 0 : _a2$b.call(_a2, ...c, 1); +(_a$b = (_a3 = a).b) === null || _a$b === void 0 ? void 0 : _a$b.call(_a3, ...c, 1); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/exec.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/exec.mjs new file mode 100644 index 000000000000..31b943a8b0cd --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/exec.mjs @@ -0,0 +1,13 @@ +class C { + #m; + constructor() { + const o = null; + const n = this; + const p = o?.#m(...c, 1); + const q = n?.#m?.(...c, 1); + expect(p).toBe(undefined); + expect(q).toBe(undefined); + } +} + +new C(); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/input.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/input.mjs new file mode 100644 index 000000000000..d133b837cb58 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/input.mjs @@ -0,0 +1,9 @@ +class C { + #m; + constructor() { + const o = null; + const n = this; + const p = o?.#m(...c, 1); + const q = n?.#m?.(...c, 1); + } +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/options.json b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/options.json new file mode 100644 index 000000000000..1b1cd54783e0 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "proposal-class-properties", "proposal-private-methods", "proposal-optional-chaining", "bugfix-v8-spread-parameters-in-optional-chaining" + ] +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/output.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/output.mjs new file mode 100644 index 000000000000..c3d3c2a6c020 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration-optional-chaining/output.mjs @@ -0,0 +1,18 @@ +var _m = new WeakMap(); + +class C { + constructor() { + var _babelHelpers$classPr; + + _m.set(this, { + writable: true, + value: void 0 + }); + + const o = null; + const n = this; + const p = o === null || o === void 0 ? void 0 : babelHelpers.classPrivateFieldGet(o, _m).call(o, ...c, 1); + const q = n === null || n === void 0 ? void 0 : (_babelHelpers$classPr = babelHelpers.classPrivateFieldGet(n, _m)) === null || _babelHelpers$classPr === void 0 ? void 0 : _babelHelpers$classPr.call(n, ...c, 1); + } + +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/exec.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/exec.mjs new file mode 100644 index 000000000000..31b943a8b0cd --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/exec.mjs @@ -0,0 +1,13 @@ +class C { + #m; + constructor() { + const o = null; + const n = this; + const p = o?.#m(...c, 1); + const q = n?.#m?.(...c, 1); + expect(p).toBe(undefined); + expect(q).toBe(undefined); + } +} + +new C(); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/input.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/input.mjs new file mode 100644 index 000000000000..d133b837cb58 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/input.mjs @@ -0,0 +1,9 @@ +class C { + #m; + constructor() { + const o = null; + const n = this; + const p = o?.#m(...c, 1); + const q = n?.#m?.(...c, 1); + } +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/options.json b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/options.json new file mode 100644 index 000000000000..540df80cbf42 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "proposal-class-properties", "proposal-private-methods", "bugfix-v8-spread-parameters-in-optional-chaining" + ] +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/output.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/output.mjs new file mode 100644 index 000000000000..c3d3c2a6c020 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private-integration/output.mjs @@ -0,0 +1,18 @@ +var _m = new WeakMap(); + +class C { + constructor() { + var _babelHelpers$classPr; + + _m.set(this, { + writable: true, + value: void 0 + }); + + const o = null; + const n = this; + const p = o === null || o === void 0 ? void 0 : babelHelpers.classPrivateFieldGet(o, _m).call(o, ...c, 1); + const q = n === null || n === void 0 ? void 0 : (_babelHelpers$classPr = babelHelpers.classPrivateFieldGet(n, _m)) === null || _babelHelpers$classPr === void 0 ? void 0 : _babelHelpers$classPr.call(n, ...c, 1); + } + +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/exec.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/exec.mjs new file mode 100644 index 000000000000..f6605bf43e8e --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/exec.mjs @@ -0,0 +1,13 @@ +class C { + #m; + constructor() { + const o = null; + const n = this; + const p = o?.#m(...c, 1); + const q = n?.#m?.(...c, 1); + expect(p).toBe(undefined); + expect(q).toBe(undefined); + } +} + +new C; diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/input.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/input.mjs new file mode 100644 index 000000000000..d133b837cb58 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/input.mjs @@ -0,0 +1,9 @@ +class C { + #m; + constructor() { + const o = null; + const n = this; + const p = o?.#m(...c, 1); + const q = n?.#m?.(...c, 1); + } +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/options.json b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/options.json new file mode 100644 index 000000000000..2e36065cd759 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/options.json @@ -0,0 +1,10 @@ +{ + "minNodeVersion": "14.0.0", + "parserOpts": { + "plugins": [ + "classPrivateMethods", + "classPrivateProperties", + "classProperties" + ] + } +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/output.mjs b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/output.mjs new file mode 100644 index 000000000000..05a520f1bef9 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/basic/class-private/output.mjs @@ -0,0 +1,13 @@ +class C { + #m; + + constructor() { + var _n$m; + + const o = null; + const n = this; + const p = o === null || o === void 0 ? void 0 : o.#m(...c, 1); + const q = n === null || n === void 0 ? void 0 : (_n$m = n.#m) === null || _n$m === void 0 ? void 0 : _n$m.call(n, ...c, 1); + } + +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/options.json b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/options.json new file mode 100644 index 000000000000..f72862b7451d --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/fixtures/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["bugfix-v8-spread-parameters-in-optional-chaining"] +} diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/index.js b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/index.js new file mode 100644 index 000000000000..21a55ce6b5e7 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/index.js @@ -0,0 +1,3 @@ +import runner from "@babel/helper-plugin-test-runner"; + +runner(import.meta.url); diff --git a/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/util.test.js b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/util.test.js new file mode 100644 index 000000000000..52f4008fcf08 --- /dev/null +++ b/packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/test/util.test.js @@ -0,0 +1,115 @@ +import { parseSync, traverse } from "@babel/core"; +import { shouldTransform } from "../src/util.ts"; + +function getPath(input, parserOpts = {}) { + let targetPath; + traverse( + parseSync(input, { + parserOpts: { + plugins: [ + "classPrivateMethods", + "classPrivateProperties", + "classProperties", + ...(parserOpts.plugins || []), + ], + ...parserOpts, + }, + filename: "example.js", + }), + { + "OptionalMemberExpression|OptionalCallExpression"(path) { + targetPath = path; + path.stop(); + }, + noScope: true, + }, + ); + return targetPath; +} + +describe("shouldTransform", () => { + const positiveCases = [ + "fn?.(...[], 0)", + "fn?.(...[], ...[])", + "fn?.(0, ...[], ...[])", + "a?.b(...[], 0)", + "a?.[b](...[], 0)", + "a?.b?.(...[], 0)", + "fn?.(0, ...[], 0)", + "a?.b?.(0, ...[], 0)", + "(a?.b)?.(...[], 0)", + "a?.b.c?.(...[], 0)", + "class C { #c; p = obj?.#c(...[], 0) }", + "class C { #c; p = obj.#c?.(...[], 0) }", + ]; + + const negativeCases = [ + "a?.b", + "fn?.(1)", + "fn?.(...[])", + "fn?.(1, ...[])", + "a?.b(...[])", + "a?.()(...[], 1)", // optional call under optional call is not affected + "(a?.b)(...[], 1)", // not an optional call + "a?.b.c(...[], 1)", + "a?.[fn?.(...[], 0)]", // optional chain in property will be handled when traversed + "a?.(fn?.(...[], 0))", // optional chain in arguments will be handled when traversed + "class C { #c; p = obj?.#c(...[]) }", + ]; + + const typescriptPositiveCases = [ + "(a?.(...[], 0) as any)?.b", + "(a?.(...[], 0) as any)?.()", + ]; + + const typescriptNegativeCases = ["(a?.b as any)(...[], 0)"]; + + describe("default parser options", () => { + test.each(positiveCases)( + "shouldTransform(a?.b in %p) should return true", + input => { + expect(shouldTransform(getPath(input))).toBe(true); + }, + ); + test.each(negativeCases)( + "shouldTransform(a?.b in %p) should return false", + input => { + expect(shouldTransform(getPath(input))).toBe(false); + }, + ); + }); + + describe("createParenthesizedExpressions", () => { + test.each(positiveCases)( + "shouldTransform(a?.b in %p with { createParenthesizedExpressions: true }) should return true", + input => { + const parserOpts = { createParenthesizedExpressions: true }; + expect(shouldTransform(getPath(input, parserOpts))).toBe(true); + }, + ); + test.each(negativeCases)( + "shouldTransform(a?.b in %p with { createParenthesizedExpressions: true }) should return false", + input => { + const parserOpts = { createParenthesizedExpressions: true }; + expect(shouldTransform(getPath(input, parserOpts))).toBe(false); + }, + ); + }); + + describe("plugins: [typescript]", () => { + test.each(positiveCases.concat(typescriptPositiveCases))( + "shouldTransform(a?.b in %p with { plugins: [typescript] }) should return true", + input => { + const parserOpts = { plugins: ["typescript"] }; + expect(shouldTransform(getPath(input, parserOpts))).toBe(true); + }, + ); + test.each(negativeCases.concat(typescriptNegativeCases))( + "shouldTransform(a?.b in %p with { plugins: [typescript] }) should return false", + input => { + const parserOpts = { plugins: ["typescript"] }; + expect(shouldTransform(getPath(input, parserOpts))).toBe(false); + }, + ); + }); +}); diff --git a/packages/babel-plugin-proposal-optional-chaining/src/index.js b/packages/babel-plugin-proposal-optional-chaining/src/index.js index d6e72baa0f98..7d3b31240df0 100644 --- a/packages/babel-plugin-proposal-optional-chaining/src/index.js +++ b/packages/babel-plugin-proposal-optional-chaining/src/index.js @@ -1,13 +1,6 @@ import { declare } from "@babel/helper-plugin-utils"; -import { - isTransparentExprWrapper, - skipTransparentExprWrappers, -} from "@babel/helper-skip-transparent-expression-wrappers"; import syntaxOptionalChaining from "@babel/plugin-syntax-optional-chaining"; -import { types as t, template } from "@babel/core"; -import { willPathCastToBoolean, findOutermostTransparentParent } from "./util"; - -const { ast } = template.expression; +import { transform } from "./transform"; export default declare((api, options) => { api.assertVersion(7); @@ -16,229 +9,16 @@ export default declare((api, options) => { const noDocumentAll = api.assumption("noDocumentAll") ?? loose; const pureGetters = api.assumption("pureGetters") ?? loose; - function isSimpleMemberExpression(expression) { - expression = skipTransparentExprWrappers(expression); - return ( - t.isIdentifier(expression) || - t.isSuper(expression) || - (t.isMemberExpression(expression) && - !expression.computed && - isSimpleMemberExpression(expression.object)) - ); - } - - /** - * Test if a given optional chain `path` needs to be memoized - * @param {NodePath} path - * @returns {boolean} - */ - function needsMemoize(path) { - let optionalPath = path; - const { scope } = path; - while ( - optionalPath.isOptionalMemberExpression() || - optionalPath.isOptionalCallExpression() - ) { - const { node } = optionalPath; - const childKey = optionalPath.isOptionalMemberExpression() - ? "object" - : "callee"; - const childPath = skipTransparentExprWrappers(optionalPath.get(childKey)); - if (node.optional) { - return !scope.isStatic(childPath.node); - } - - optionalPath = childPath; - } - } - return { name: "proposal-optional-chaining", inherits: syntaxOptionalChaining, visitor: { "OptionalCallExpression|OptionalMemberExpression"(path) { - const { scope } = path; - // maybeWrapped points to the outermost transparent expression wrapper - // or the path itself - const maybeWrapped = findOutermostTransparentParent(path); - const { parentPath } = maybeWrapped; - const willReplacementCastToBoolean = willPathCastToBoolean( - maybeWrapped, - ); - let isDeleteOperation = false; - const parentIsCall = - parentPath.isCallExpression({ callee: maybeWrapped.node }) && - // note that the first condition must implies that `path.optional` is `true`, - // otherwise the parentPath should be an OptionalCallExpressioin - path.isOptionalMemberExpression(); - - const optionals = []; - - let optionalPath = path; - // Replace `function (a, x = a.b?.c) {}` to `function (a, x = (() => a.b?.c)() ){}` - // so the temporary variable can be injected in correct scope - if (scope.path.isPattern() && needsMemoize(optionalPath)) { - path.replaceWith(template.ast`(() => ${path.node})()`); - // The injected optional chain will be queued and eventually transformed when visited - return; - } - while ( - optionalPath.isOptionalMemberExpression() || - optionalPath.isOptionalCallExpression() - ) { - const { node } = optionalPath; - if (node.optional) { - optionals.push(node); - } - - if (optionalPath.isOptionalMemberExpression()) { - optionalPath.node.type = "MemberExpression"; - optionalPath = skipTransparentExprWrappers( - optionalPath.get("object"), - ); - } else if (optionalPath.isOptionalCallExpression()) { - optionalPath.node.type = "CallExpression"; - optionalPath = skipTransparentExprWrappers( - optionalPath.get("callee"), - ); - } - } - - let replacementPath = path; - if (parentPath.isUnaryExpression({ operator: "delete" })) { - replacementPath = parentPath; - isDeleteOperation = true; - } - for (let i = optionals.length - 1; i >= 0; i--) { - const node = optionals[i]; - - const isCall = t.isCallExpression(node); - const replaceKey = isCall ? "callee" : "object"; - - const chainWithTypes = node[replaceKey]; - let chain = chainWithTypes; - - while (isTransparentExprWrapper(chain)) { - chain = chain.expression; - } - - let ref; - let check; - if (isCall && t.isIdentifier(chain, { name: "eval" })) { - check = ref = chain; - // `eval?.()` is an indirect eval call transformed to `(0,eval)()` - node[replaceKey] = t.sequenceExpression([t.numericLiteral(0), ref]); - } else if (pureGetters && isCall && isSimpleMemberExpression(chain)) { - // If we assume getters are pure (avoiding a Function#call) and we are at the call, - // we can avoid a needless memoize. We only do this if the callee is a simple member - // expression, to avoid multiple calls to nested call expressions. - check = ref = chainWithTypes; - } else { - ref = scope.maybeGenerateMemoised(chain); - if (ref) { - check = t.assignmentExpression( - "=", - t.cloneNode(ref), - // Here `chainWithTypes` MUST NOT be cloned because it could be - // updated when generating the memoised context of a call - // expression - chainWithTypes, - ); - - node[replaceKey] = ref; - } else { - check = ref = chainWithTypes; - } - } - - // Ensure call expressions have the proper `this` - // `foo.bar()` has context `foo`. - if (isCall && t.isMemberExpression(chain)) { - if (pureGetters && isSimpleMemberExpression(chain)) { - // To avoid a Function#call, we can instead re-grab the property from the context object. - // `a.?b.?()` translates roughly to `_a.b != null && _a.b()` - node.callee = chainWithTypes; - } else { - // Otherwise, we need to memoize the context object, and change the call into a Function#call. - // `a.?b.?()` translates roughly to `(_b = _a.b) != null && _b.call(_a)` - const { object } = chain; - let context = scope.maybeGenerateMemoised(object); - if (context) { - chain.object = t.assignmentExpression("=", context, object); - } else if (t.isSuper(object)) { - context = t.thisExpression(); - } else { - context = object; - } - - node.arguments.unshift(t.cloneNode(context)); - node.callee = t.memberExpression( - node.callee, - t.identifier("call"), - ); - } - } - let replacement = replacementPath.node; - // Ensure (a?.b)() has proper `this` - // The `parentIsCall` is constant within loop, we should check i === 0 - // to ensure that it is only applied to the first optional chain element - // i.e. `?.b` in `(a?.b.c)()` - if (i === 0 && parentIsCall) { - // `(a?.b)()` to `(a == null ? undefined : a.b.bind(a))()` - const object = skipTransparentExprWrappers( - replacementPath.get("object"), - ).node; - let baseRef; - if (!pureGetters || !isSimpleMemberExpression(object)) { - // memoize the context object when getters are not always pure - // or the object is not a simple member expression - // `(a?.b.c)()` to `(a == null ? undefined : (_a$b = a.b).c.bind(_a$b))()` - baseRef = scope.maybeGenerateMemoised(object); - if (baseRef) { - replacement.object = t.assignmentExpression( - "=", - baseRef, - object, - ); - } - } - replacement = t.callExpression( - t.memberExpression(replacement, t.identifier("bind")), - [t.cloneNode(baseRef ?? object)], - ); - } - - if (willReplacementCastToBoolean) { - // `if (a?.b) {}` transformed to `if (a != null && a.b) {}` - // we don't need to return `void 0` because the returned value will - // eveutally cast to boolean. - const nonNullishCheck = noDocumentAll - ? ast`${t.cloneNode(check)} != null` - : ast` - ${t.cloneNode(check)} !== null && ${t.cloneNode(ref)} !== void 0`; - replacementPath.replaceWith( - t.logicalExpression("&&", nonNullishCheck, replacement), - ); - replacementPath = skipTransparentExprWrappers( - replacementPath.get("right"), - ); - } else { - const nullishCheck = noDocumentAll - ? ast`${t.cloneNode(check)} == null` - : ast` - ${t.cloneNode(check)} === null || ${t.cloneNode(ref)} === void 0`; - - const returnValue = isDeleteOperation ? ast`true` : ast`void 0`; - replacementPath.replaceWith( - t.conditionalExpression(nullishCheck, returnValue, replacement), - ); - replacementPath = skipTransparentExprWrappers( - replacementPath.get("alternate"), - ); - } - } + transform(path, { noDocumentAll, pureGetters }); }, }, }; }); + +export { transform }; diff --git a/packages/babel-plugin-proposal-optional-chaining/src/transform.js b/packages/babel-plugin-proposal-optional-chaining/src/transform.js new file mode 100644 index 000000000000..8bd82a56cb1e --- /dev/null +++ b/packages/babel-plugin-proposal-optional-chaining/src/transform.js @@ -0,0 +1,219 @@ +import { types as t, template } from "@babel/core"; +import { + isTransparentExprWrapper, + skipTransparentExprWrappers, +} from "@babel/helper-skip-transparent-expression-wrappers"; +import { willPathCastToBoolean, findOutermostTransparentParent } from "./util"; + +const { ast } = template.expression; + +function isSimpleMemberExpression(expression) { + expression = skipTransparentExprWrappers(expression); + return ( + t.isIdentifier(expression) || + t.isSuper(expression) || + (t.isMemberExpression(expression) && + !expression.computed && + isSimpleMemberExpression(expression.object)) + ); +} + +/** + * Test if a given optional chain `path` needs to be memoized + * @param {NodePath} path + * @returns {boolean} + */ +function needsMemoize(path) { + let optionalPath = path; + const { scope } = path; + while ( + optionalPath.isOptionalMemberExpression() || + optionalPath.isOptionalCallExpression() + ) { + const { node } = optionalPath; + const childKey = optionalPath.isOptionalMemberExpression() + ? "object" + : "callee"; + const childPath = skipTransparentExprWrappers(optionalPath.get(childKey)); + if (node.optional) { + return !scope.isStatic(childPath.node); + } + + optionalPath = childPath; + } +} + +export function transform( + path: NodePath, + { + pureGetters, + noDocumentAll, + }: { pureGetters: boolean, noDocumentAll: boolean }, +) { + const { scope } = path; + // maybeWrapped points to the outermost transparent expression wrapper + // or the path itself + const maybeWrapped = findOutermostTransparentParent(path); + const { parentPath } = maybeWrapped; + const willReplacementCastToBoolean = willPathCastToBoolean(maybeWrapped); + let isDeleteOperation = false; + const parentIsCall = + parentPath.isCallExpression({ callee: maybeWrapped.node }) && + // note that the first condition must implies that `path.optional` is `true`, + // otherwise the parentPath should be an OptionalCallExpression + path.isOptionalMemberExpression(); + + const optionals = []; + + let optionalPath = path; + // Replace `function (a, x = a.b?.c) {}` to `function (a, x = (() => a.b?.c)() ){}` + // so the temporary variable can be injected in correct scope + if (scope.path.isPattern() && needsMemoize(optionalPath)) { + path.replaceWith(template.ast`(() => ${path.node})()`); + // The injected optional chain will be queued and eventually transformed when visited + return; + } + while ( + optionalPath.isOptionalMemberExpression() || + optionalPath.isOptionalCallExpression() + ) { + const { node } = optionalPath; + if (node.optional) { + optionals.push(node); + } + + if (optionalPath.isOptionalMemberExpression()) { + optionalPath.node.type = "MemberExpression"; + optionalPath = skipTransparentExprWrappers(optionalPath.get("object")); + } else if (optionalPath.isOptionalCallExpression()) { + optionalPath.node.type = "CallExpression"; + optionalPath = skipTransparentExprWrappers(optionalPath.get("callee")); + } + } + + let replacementPath = path; + if (parentPath.isUnaryExpression({ operator: "delete" })) { + replacementPath = parentPath; + isDeleteOperation = true; + } + for (let i = optionals.length - 1; i >= 0; i--) { + const node = optionals[i]; + + const isCall = t.isCallExpression(node); + const replaceKey = isCall ? "callee" : "object"; + + const chainWithTypes = node[replaceKey]; + let chain = chainWithTypes; + + while (isTransparentExprWrapper(chain)) { + chain = chain.expression; + } + + let ref; + let check; + if (isCall && t.isIdentifier(chain, { name: "eval" })) { + check = ref = chain; + // `eval?.()` is an indirect eval call transformed to `(0,eval)()` + node[replaceKey] = t.sequenceExpression([t.numericLiteral(0), ref]); + } else if (pureGetters && isCall && isSimpleMemberExpression(chain)) { + // If we assume getters are pure (avoiding a Function#call) and we are at the call, + // we can avoid a needless memoize. We only do this if the callee is a simple member + // expression, to avoid multiple calls to nested call expressions. + check = ref = chainWithTypes; + } else { + ref = scope.maybeGenerateMemoised(chain); + if (ref) { + check = t.assignmentExpression( + "=", + t.cloneNode(ref), + // Here `chainWithTypes` MUST NOT be cloned because it could be + // updated when generating the memoised context of a call + // expression + chainWithTypes, + ); + + node[replaceKey] = ref; + } else { + check = ref = chainWithTypes; + } + } + + // Ensure call expressions have the proper `this` + // `foo.bar()` has context `foo`. + if (isCall && t.isMemberExpression(chain)) { + if (pureGetters && isSimpleMemberExpression(chain)) { + // To avoid a Function#call, we can instead re-grab the property from the context object. + // `a.?b.?()` translates roughly to `_a.b != null && _a.b()` + node.callee = chainWithTypes; + } else { + // Otherwise, we need to memoize the context object, and change the call into a Function#call. + // `a.?b.?()` translates roughly to `(_b = _a.b) != null && _b.call(_a)` + const { object } = chain; + let context = scope.maybeGenerateMemoised(object); + if (context) { + chain.object = t.assignmentExpression("=", context, object); + } else if (t.isSuper(object)) { + context = t.thisExpression(); + } else { + context = object; + } + + node.arguments.unshift(t.cloneNode(context)); + node.callee = t.memberExpression(node.callee, t.identifier("call")); + } + } + let replacement = replacementPath.node; + // Ensure (a?.b)() has proper `this` + // The `parentIsCall` is constant within loop, we should check i === 0 + // to ensure that it is only applied to the first optional chain element + // i.e. `?.b` in `(a?.b.c)()` + if (i === 0 && parentIsCall) { + // `(a?.b)()` to `(a == null ? undefined : a.b.bind(a))()` + const object = skipTransparentExprWrappers(replacementPath.get("object")) + .node; + let baseRef; + if (!pureGetters || !isSimpleMemberExpression(object)) { + // memoize the context object when getters are not always pure + // or the object is not a simple member expression + // `(a?.b.c)()` to `(a == null ? undefined : (_a$b = a.b).c.bind(_a$b))()` + baseRef = scope.maybeGenerateMemoised(object); + if (baseRef) { + replacement.object = t.assignmentExpression("=", baseRef, object); + } + } + replacement = t.callExpression( + t.memberExpression(replacement, t.identifier("bind")), + [t.cloneNode(baseRef ?? object)], + ); + } + + if (willReplacementCastToBoolean) { + // `if (a?.b) {}` transformed to `if (a != null && a.b) {}` + // we don't need to return `void 0` because the returned value will + // eveutally cast to boolean. + const nonNullishCheck = noDocumentAll + ? ast`${t.cloneNode(check)} != null` + : ast` + ${t.cloneNode(check)} !== null && ${t.cloneNode(ref)} !== void 0`; + replacementPath.replaceWith( + t.logicalExpression("&&", nonNullishCheck, replacement), + ); + replacementPath = skipTransparentExprWrappers( + replacementPath.get("right"), + ); + } else { + const nullishCheck = noDocumentAll + ? ast`${t.cloneNode(check)} == null` + : ast` + ${t.cloneNode(check)} === null || ${t.cloneNode(ref)} === void 0`; + + const returnValue = isDeleteOperation ? ast`true` : ast`void 0`; + replacementPath.replaceWith( + t.conditionalExpression(nullishCheck, returnValue, replacement), + ); + replacementPath = skipTransparentExprWrappers( + replacementPath.get("alternate"), + ); + } + } +} diff --git a/packages/babel-preset-env/package.json b/packages/babel-preset-env/package.json index 1b4df0f078c4..f5f49f342a25 100644 --- a/packages/babel-preset-env/package.json +++ b/packages/babel-preset-env/package.json @@ -20,6 +20,7 @@ "@babel/helper-compilation-targets": "workspace:^7.13.10", "@babel/helper-plugin-utils": "workspace:^7.13.0", "@babel/helper-validator-option": "workspace:^7.12.17", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "workspace:^7.13.8", "@babel/plugin-proposal-async-generator-functions": "workspace:^7.13.8", "@babel/plugin-proposal-class-properties": "workspace:^7.13.0", "@babel/plugin-proposal-dynamic-import": "workspace:^7.13.8", diff --git a/packages/babel-preset-env/src/available-plugins.js b/packages/babel-preset-env/src/available-plugins.js index 99a09ac1ee2b..4da14d528b4e 100644 --- a/packages/babel-preset-env/src/available-plugins.js +++ b/packages/babel-preset-env/src/available-plugins.js @@ -65,6 +65,7 @@ import bugfixEdgeFunctionName from "@babel/preset-modules/lib/plugins/transform- import bugfixTaggedTemplateCaching from "@babel/preset-modules/lib/plugins/transform-tagged-template-caching"; import bugfixSafariBlockShadowing from "@babel/preset-modules/lib/plugins/transform-safari-block-shadowing"; import bugfixSafariForShadowing from "@babel/preset-modules/lib/plugins/transform-safari-for-shadowing"; +import bugfixV8SpreadParametersInOptionalChaining from "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining"; export default { "bugfix/transform-async-arrows-in-class": bugfixAsyncArrowsInClass, @@ -73,6 +74,7 @@ export default { "bugfix/transform-safari-block-shadowing": bugfixSafariBlockShadowing, "bugfix/transform-safari-for-shadowing": bugfixSafariForShadowing, "bugfix/transform-tagged-template-caching": bugfixTaggedTemplateCaching, + "bugfix/transform-v8-spread-parameters-in-optional-chaining": bugfixV8SpreadParametersInOptionalChaining, "proposal-async-generator-functions": proposalAsyncGeneratorFunctions, "proposal-class-properties": proposalClassProperties, "proposal-dynamic-import": proposalDynamicImport, diff --git a/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/input.js b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/input.js new file mode 100644 index 000000000000..0c621dd567cd --- /dev/null +++ b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/input.js @@ -0,0 +1,3 @@ +fn?.(); + +fn?.(...[], 0); diff --git a/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/options.json b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/options.json new file mode 100644 index 000000000000..d68a64957b8d --- /dev/null +++ b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/options.json @@ -0,0 +1,12 @@ +{ + "validateLogs": true, + "presets": [ + ["env", { + "debug": true, + "bugfixes": false, + "targets": { + "chrome": "89" + } + }] + ] +} diff --git a/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/output.js b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/output.js new file mode 100644 index 000000000000..78cc3e629074 --- /dev/null +++ b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/output.js @@ -0,0 +1,4 @@ +var _fn, _fn2; + +(_fn = fn) === null || _fn === void 0 ? void 0 : _fn(); +(_fn2 = fn) === null || _fn2 === void 0 ? void 0 : _fn2(...[], 0); diff --git a/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/stdout.txt b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/stdout.txt new file mode 100644 index 000000000000..f5190f4077d5 --- /dev/null +++ b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89-no-bugfixes/stdout.txt @@ -0,0 +1,22 @@ +@babel/preset-env: `DEBUG` option + +Using targets: +{ + "chrome": "89" +} + +Using modules transform: auto + +Using plugins: + syntax-numeric-separator { "chrome":"89" } + syntax-nullish-coalescing-operator { "chrome":"89" } + proposal-optional-chaining { "chrome":"89" } + syntax-json-strings { "chrome":"89" } + syntax-optional-catch-binding { "chrome":"89" } + syntax-async-generators { "chrome":"89" } + syntax-object-rest-spread { "chrome":"89" } + transform-modules-commonjs { "chrome":"89" } + proposal-dynamic-import { "chrome":"89" } + proposal-export-namespace-from {} + +Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set. diff --git a/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/input.js b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/input.js new file mode 100644 index 000000000000..0c621dd567cd --- /dev/null +++ b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/input.js @@ -0,0 +1,3 @@ +fn?.(); + +fn?.(...[], 0); diff --git a/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/options.json b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/options.json new file mode 100644 index 000000000000..9b16c181dbc0 --- /dev/null +++ b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/options.json @@ -0,0 +1,12 @@ +{ + "validateLogs": true, + "presets": [ + ["env", { + "debug": true, + "bugfixes": true, + "targets": { + "chrome": "89" + } + }] + ] +} diff --git a/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/output.js b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/output.js new file mode 100644 index 000000000000..7836a896f5f4 --- /dev/null +++ b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/output.js @@ -0,0 +1,4 @@ +var _fn; + +fn?.(); +(_fn = fn) === null || _fn === void 0 ? void 0 : _fn(...[], 0); diff --git a/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/stdout.txt b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/stdout.txt new file mode 100644 index 000000000000..629cb295c6ed --- /dev/null +++ b/packages/babel-preset-env/test/fixtures/bugfixes/v8-spread-parameters-in-optional-chaining-chrome-89/stdout.txt @@ -0,0 +1,23 @@ +@babel/preset-env: `DEBUG` option + +Using targets: +{ + "chrome": "89" +} + +Using modules transform: auto + +Using plugins: + syntax-numeric-separator { "chrome":"89" } + syntax-nullish-coalescing-operator { "chrome":"89" } + syntax-optional-chaining { "chrome":"89" } + syntax-json-strings { "chrome":"89" } + syntax-optional-catch-binding { "chrome":"89" } + syntax-async-generators { "chrome":"89" } + syntax-object-rest-spread { "chrome":"89" } + bugfix/transform-v8-spread-parameters-in-optional-chaining { "chrome":"89" } + transform-modules-commonjs { "chrome":"89" } + proposal-dynamic-import { "chrome":"89" } + proposal-export-namespace-from {} + +Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set. diff --git a/yarn.lock b/yarn.lock index c02527fceead..a23d0f621bdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -973,6 +973,21 @@ __metadata: languageName: unknown linkType: soft +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@workspace:^7.13.8, @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@workspace:packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining": + version: 0.0.0-use.local + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@workspace:packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining" + dependencies: + "@babel/core": "workspace:*" + "@babel/helper-plugin-test-runner": "workspace:*" + "@babel/helper-plugin-utils": "workspace:^7.13.0" + "@babel/helper-skip-transparent-expression-wrappers": "workspace:^7.12.1" + "@babel/plugin-proposal-optional-chaining": "workspace:^7.13.8" + "@babel/traverse": "workspace:*" + peerDependencies: + "@babel/core": ^7.13.0 + languageName: unknown + linkType: soft + "@babel/plugin-codemod-object-assign-to-object-spread@workspace:codemods/babel-plugin-codemod-object-assign-to-object-spread": version: 0.0.0-use.local resolution: "@babel/plugin-codemod-object-assign-to-object-spread@workspace:codemods/babel-plugin-codemod-object-assign-to-object-spread" @@ -3068,6 +3083,7 @@ __metadata: "@babel/helper-plugin-test-runner": "workspace:*" "@babel/helper-plugin-utils": "workspace:^7.13.0" "@babel/helper-validator-option": "workspace:^7.12.17" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "workspace:^7.13.8" "@babel/plugin-proposal-async-generator-functions": "workspace:^7.13.8" "@babel/plugin-proposal-class-properties": "workspace:^7.13.0" "@babel/plugin-proposal-dynamic-import": "workspace:^7.13.8"