Implement @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining (#13009)

This commit is contained in:
Huáng Jùnliàng 2021-03-19 13:26:28 -04:00 committed by GitHub
parent 974f9c5b01
commit c2a42492db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 807 additions and 227 deletions

View File

@ -426,6 +426,7 @@ const libBundles = [
"packages/babel-plugin-proposal-optional-chaining", "packages/babel-plugin-proposal-optional-chaining",
"packages/babel-preset-typescript", "packages/babel-preset-typescript",
"packages/babel-helper-member-expression-to-functions", "packages/babel-helper-member-expression-to-functions",
"packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining",
].map(src => ({ ].map(src => ({
src, src,
format: "cjs", format: "cjs",

View File

@ -166,7 +166,9 @@ declare module "@babel/helper-function-name" {
} }
declare module "@babel/helper-split-export-declaration" { 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" { declare module "@babel/traverse" {
@ -196,6 +198,13 @@ declare module "@babel/highlight" {
/** /**
* Highlight `code`. * Highlight `code`.
*/ */
declare export default function highlight(code: string, options?: Options): string; declare export default function highlight(
declare export { getChalk, shouldHighlight }; code: string,
options?: Options
): string;
declare export { getChalk, shouldHighlight };
}
declare module "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" {
declare module.exports: any;
} }

View File

@ -0,0 +1,3 @@
src
test
*.log

View File

@ -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
```

View File

@ -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:*"
}
}

View File

@ -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 });
}
},
},
};
});

View File

@ -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<t.OptionalMemberExpression | t.OptionalCallExpression>)} path
* @returns {boolean}
*/
export function shouldTransform(
path: NodePath<t.OptionalMemberExpression | t.OptionalCallExpression>,
): 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;
}

View File

@ -0,0 +1,7 @@
fn?.(...b, 1);
a?.b(...c, 1);
a?.b?.(...c, 1);
a.b?.(...c, 1);

View File

@ -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);

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,5 @@
fn?.(...b, 1);
a?.b(...c, 1);
a?.b?.(...c, 1);

View File

@ -0,0 +1,5 @@
{
"plugins": [
"proposal-optional-chaining", "bugfix-v8-spread-parameters-in-optional-chaining"
]
}

View File

@ -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);

View File

@ -0,0 +1,7 @@
fn?.(...b, 1);
a?.b(...c, 1);
a?.b?.(...c, 1);
a.b?.(...c, 1);

View File

@ -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);

View File

@ -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();

View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
{
"plugins": [
"proposal-class-properties", "proposal-private-methods", "proposal-optional-chaining", "bugfix-v8-spread-parameters-in-optional-chaining"
]
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
{
"plugins": [
"proposal-class-properties", "proposal-private-methods", "bugfix-v8-spread-parameters-in-optional-chaining"
]
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -0,0 +1,10 @@
{
"minNodeVersion": "14.0.0",
"parserOpts": {
"plugins": [
"classPrivateMethods",
"classPrivateProperties",
"classProperties"
]
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
{
"plugins": ["bugfix-v8-spread-parameters-in-optional-chaining"]
}

View File

@ -0,0 +1,3 @@
import runner from "@babel/helper-plugin-test-runner";
runner(import.meta.url);

View File

@ -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);
},
);
});
});

View File

@ -1,13 +1,6 @@
import { declare } from "@babel/helper-plugin-utils"; 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 syntaxOptionalChaining from "@babel/plugin-syntax-optional-chaining";
import { types as t, template } from "@babel/core"; import { transform } from "./transform";
import { willPathCastToBoolean, findOutermostTransparentParent } from "./util";
const { ast } = template.expression;
export default declare((api, options) => { export default declare((api, options) => {
api.assertVersion(7); api.assertVersion(7);
@ -16,229 +9,16 @@ export default declare((api, options) => {
const noDocumentAll = api.assumption("noDocumentAll") ?? loose; const noDocumentAll = api.assumption("noDocumentAll") ?? loose;
const pureGetters = api.assumption("pureGetters") ?? 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 { return {
name: "proposal-optional-chaining", name: "proposal-optional-chaining",
inherits: syntaxOptionalChaining, inherits: syntaxOptionalChaining,
visitor: { visitor: {
"OptionalCallExpression|OptionalMemberExpression"(path) { "OptionalCallExpression|OptionalMemberExpression"(path) {
const { scope } = path; transform(path, { noDocumentAll, pureGetters });
// 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"),
);
}
}
}, },
}, },
}; };
}); });
export { transform };

View File

@ -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<t.OptionalCallExpression | t.OptionalMemberExpression>,
{
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"),
);
}
}
}

View File

@ -20,6 +20,7 @@
"@babel/helper-compilation-targets": "workspace:^7.13.10", "@babel/helper-compilation-targets": "workspace:^7.13.10",
"@babel/helper-plugin-utils": "workspace:^7.13.0", "@babel/helper-plugin-utils": "workspace:^7.13.0",
"@babel/helper-validator-option": "workspace:^7.12.17", "@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-async-generator-functions": "workspace:^7.13.8",
"@babel/plugin-proposal-class-properties": "workspace:^7.13.0", "@babel/plugin-proposal-class-properties": "workspace:^7.13.0",
"@babel/plugin-proposal-dynamic-import": "workspace:^7.13.8", "@babel/plugin-proposal-dynamic-import": "workspace:^7.13.8",

View File

@ -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 bugfixTaggedTemplateCaching from "@babel/preset-modules/lib/plugins/transform-tagged-template-caching";
import bugfixSafariBlockShadowing from "@babel/preset-modules/lib/plugins/transform-safari-block-shadowing"; 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 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 { export default {
"bugfix/transform-async-arrows-in-class": bugfixAsyncArrowsInClass, "bugfix/transform-async-arrows-in-class": bugfixAsyncArrowsInClass,
@ -73,6 +74,7 @@ export default {
"bugfix/transform-safari-block-shadowing": bugfixSafariBlockShadowing, "bugfix/transform-safari-block-shadowing": bugfixSafariBlockShadowing,
"bugfix/transform-safari-for-shadowing": bugfixSafariForShadowing, "bugfix/transform-safari-for-shadowing": bugfixSafariForShadowing,
"bugfix/transform-tagged-template-caching": bugfixTaggedTemplateCaching, "bugfix/transform-tagged-template-caching": bugfixTaggedTemplateCaching,
"bugfix/transform-v8-spread-parameters-in-optional-chaining": bugfixV8SpreadParametersInOptionalChaining,
"proposal-async-generator-functions": proposalAsyncGeneratorFunctions, "proposal-async-generator-functions": proposalAsyncGeneratorFunctions,
"proposal-class-properties": proposalClassProperties, "proposal-class-properties": proposalClassProperties,
"proposal-dynamic-import": proposalDynamicImport, "proposal-dynamic-import": proposalDynamicImport,

View File

@ -0,0 +1,12 @@
{
"validateLogs": true,
"presets": [
["env", {
"debug": true,
"bugfixes": false,
"targets": {
"chrome": "89"
}
}]
]
}

View File

@ -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);

View File

@ -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.

View File

@ -0,0 +1,3 @@
fn?.();
fn?.(...[], 0);

View File

@ -0,0 +1,12 @@
{
"validateLogs": true,
"presets": [
["env", {
"debug": true,
"bugfixes": true,
"targets": {
"chrome": "89"
}
}]
]
}

View File

@ -0,0 +1,4 @@
var _fn;
fn?.();
(_fn = fn) === null || _fn === void 0 ? void 0 : _fn(...[], 0);

View File

@ -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.

View File

@ -973,6 +973,21 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft 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": "@babel/plugin-codemod-object-assign-to-object-spread@workspace:codemods/babel-plugin-codemod-object-assign-to-object-spread":
version: 0.0.0-use.local 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" 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-test-runner": "workspace:*"
"@babel/helper-plugin-utils": "workspace:^7.13.0" "@babel/helper-plugin-utils": "workspace:^7.13.0"
"@babel/helper-validator-option": "workspace:^7.12.17" "@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-async-generator-functions": "workspace:^7.13.8"
"@babel/plugin-proposal-class-properties": "workspace:^7.13.0" "@babel/plugin-proposal-class-properties": "workspace:^7.13.0"
"@babel/plugin-proposal-dynamic-import": "workspace:^7.13.8" "@babel/plugin-proposal-dynamic-import": "workspace:^7.13.8"