Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optimize optional chain when expression will be cast to boolean #12291

26 changes: 18 additions & 8 deletions Gulpfile.js
Expand Up @@ -102,7 +102,13 @@ const babelVersion =
function buildRollup(packages) {
const sourcemap = process.env.NODE_ENV === "production";
return Promise.all(
packages.map(async ({ src, format, dest, name, filename, version }) => {
packages.map(async ({ src, format, dest, name, filename }) => {
const pkgJSON = require("./" + src + "/package.json");
const version = pkgJSON.version + versionSuffix;
const { dependencies = {}, peerDependencies = {} } = pkgJSON;
const external = Object.keys(dependencies).concat(
Object.keys(peerDependencies)
);
let nodeResolveBrowser = false,
babelEnvName = "rollup";
switch (src) {
Expand All @@ -115,6 +121,7 @@ function buildRollup(packages) {
fancyLog(`Compiling '${chalk.cyan(input)}' with rollup ...`);
const bundle = await rollup.rollup({
input,
external,
plugins: [
rollupBabelSource(),
rollupReplace({
Expand Down Expand Up @@ -161,6 +168,7 @@ function buildRollup(packages) {
format,
name,
sourcemap: sourcemap,
exports: "named",
});

if (!process.env.IS_PUBLISH) {
Expand All @@ -180,6 +188,7 @@ function buildRollup(packages) {
format,
name,
sourcemap: sourcemap,
exports: "named",
plugins: [
rollupTerser({
// workaround https://bugs.webkit.org/show_bug.cgi?id=212725
Expand All @@ -194,13 +203,14 @@ function buildRollup(packages) {
}

const libBundles = [
{
src: "packages/babel-parser",
format: "cjs",
dest: "lib",
version: require("./packages/babel-parser/package").version + versionSuffix,
},
];
"packages/babel-parser",
"packages/babel-plugin-proposal-optional-chaining",
"packages/babel-helper-member-expression-to-functions",
].map(src => ({
src,
format: "cjs",
dest: "lib",
}));

const standaloneBundle = [
{
Expand Down
82 changes: 56 additions & 26 deletions packages/babel-helper-member-expression-to-functions/src/index.js
@@ -1,4 +1,5 @@
import * as t from "@babel/types";
import { willPathCastToBoolean } from "./util.js";

class AssignmentMemoiser {
constructor() {
Expand Down Expand Up @@ -129,6 +130,8 @@ const handle = {
return;
}

const willEndPathCastToBoolean = willPathCastToBoolean(endPath);

const rootParentPath = endPath.parentPath;
if (
rootParentPath.isUpdateExpression({ argument: node }) ||
Expand Down Expand Up @@ -238,33 +241,60 @@ const handle = {
regular = endParentPath.node;
}

replacementPath.replaceWith(
t.conditionalExpression(
t.logicalExpression(
"||",
t.binaryExpression(
"===",
baseNeedsMemoised
? t.assignmentExpression(
"=",
t.cloneNode(baseRef),
t.cloneNode(startingNode),
)
: t.cloneNode(baseRef),
t.nullLiteral(),
),
t.binaryExpression(
"===",
t.cloneNode(baseRef),
scope.buildUndefinedNode(),
),
if (willEndPathCastToBoolean) {
const nonNullishCheck = t.logicalExpression(
"&&",
t.binaryExpression(
"!==",
baseNeedsMemoised
? t.assignmentExpression(
"=",
t.cloneNode(baseRef),
t.cloneNode(startingNode),
)
: t.cloneNode(baseRef),
t.nullLiteral(),
),
isDeleteOperation
? t.booleanLiteral(true)
: scope.buildUndefinedNode(),
regular,
),
);
t.binaryExpression(
"!==",
t.cloneNode(baseRef),
scope.buildUndefinedNode(),
),
);
replacementPath.replaceWith(
t.logicalExpression("&&", nonNullishCheck, regular),
);
} else {
// todo: respect assumptions.noDocumentAll when assumptions are implemented
const nullishCheck = t.logicalExpression(
"||",
t.binaryExpression(
"===",
baseNeedsMemoised
? t.assignmentExpression(
"=",
t.cloneNode(baseRef),
t.cloneNode(startingNode),
)
: t.cloneNode(baseRef),
t.nullLiteral(),
),
t.binaryExpression(
"===",
t.cloneNode(baseRef),
scope.buildUndefinedNode(),
),
);
replacementPath.replaceWith(
t.conditionalExpression(
nullishCheck,
isDeleteOperation
? t.booleanLiteral(true)
: scope.buildUndefinedNode(),
regular,
),
);
}

// context and isDeleteOperation can not be both truthy
if (context) {
Expand Down
45 changes: 45 additions & 0 deletions packages/babel-helper-member-expression-to-functions/src/util.js
@@ -0,0 +1,45 @@
/**
* Test if a NodePath will be cast to boolean when evaluated.
*
* @example
* // returns true
* const nodePathAQDotB = NodePath("if (a?.#b) {}").get("test"); // a?.#b
* willPathCastToBoolean(nodePathAQDotB)
* @example
* // returns false
* willPathCastToBoolean(NodePath("a?.#b"))
* @todo Respect transparent expression wrappers
* @see {@link packages/babel-plugin-proposal-optional-chaining/src/util.js}
* @param {NodePath} path
* @returns {boolean}
*/
export function willPathCastToBoolean(path: NodePath): boolean {
const maybeWrapped = path;
const { node, parentPath } = maybeWrapped;
if (parentPath.isLogicalExpression()) {
const { operator, right } = parentPath.node;
if (
operator === "&&" ||
operator === "||" ||
(operator === "??" && node === right)
) {
return willPathCastToBoolean(parentPath);
}
}
if (parentPath.isSequenceExpression()) {
const { expressions } = parentPath.node;
if (expressions[expressions.length - 1] === node) {
return willPathCastToBoolean(parentPath);
} else {
// if it is in the middle of a sequence expression, we don't
// care the return value so just cast to boolean for smaller
// output
return true;
}
}
return (
parentPath.isConditional({ test: node }) ||
parentPath.isUnaryExpression({ operator: "!" }) ||
parentPath.isLoop({ test: node })
);
}
@@ -0,0 +1,119 @@
class C {
static #a = {
b: {
c: {
d: 2,
},
},
};
static testIf(o) {
if (o?.#a.b.c.d) {
return true;
}
return false;
}
static testConditional(o) {
return o?.#a.b?.c.d ? true : false;
}
static testLoop(o) {
while (o?.#a.b.c.d) {
for (; o?.#a.b.c?.d; ) {
let i = 0;
do {
i++;
if (i === 2) {
return true;
}
} while (o?.#a.b?.c.d);
}
}
return false;
}
static testNegate(o) {
return !!o?.#a.b?.c.d;
}
static testIfDeep(o) {
if (o.obj?.#a.b?.c.d) {
return true;
}
return false;
}
static testConditionalDeep(o) {
return o.obj?.#a.b?.c.d ? true : false;
}
static testLoopDeep(o) {
while (o.obj?.#a.b.c.d) {
for (; o.obj?.#a.b.c?.d; ) {
let i = 0;
do {
i++;
if (i === 2) {
return true;
}
} while (o.obj?.#a.b?.c.d);
}
}
return false;
}
static testNegateDeep(o) {
return !!o.obj?.#a.b?.c.d;
}

static testLogicalInIf(o) {
if (o?.#a.b?.c.d && o?.#a?.b.c.d) {
return true;
}
return false;
}

static testLogicalInReturn(o) {
return o?.#a.b?.c.d && o?.#a?.b.c.d;
}

static testNullishCoalescing(o) {
if (o?.#a.b?.c.non_existent ?? o?.#a.b?.c.d) {
return o?.#a.b?.c.non_existent ?? o?.#a.b?.c.d;
}
return o?.#a.b?.c.non_existent ?? o;
}

static test() {
const c = C;
expect(C.testIf(c)).toBe(true);
expect(C.testConditional(c)).toBe(true);
expect(C.testLoop(c)).toBe(true);
expect(C.testNegate(c)).toBe(true);

expect(C.testIfDeep({ obj: c })).toBe(true);
expect(C.testConditionalDeep({ obj: c })).toBe(true);
expect(C.testLoopDeep({ obj: c })).toBe(true);
expect(C.testNegateDeep({ obj: c })).toBe(true);

expect(C.testLogicalInIf(c)).toBe(true);
expect(C.testLogicalInReturn(c)).toBe(2);

expect(C.testNullishCoalescing(c)).toBe(2);
}

static testNullish() {
for (const n of [null, undefined]) {
expect(C.testIf(n)).toBe(false);
expect(C.testConditional(n)).toBe(false);
expect(C.testLoop(n)).toBe(false);
expect(C.testNegate(n)).toBe(false);

expect(C.testIfDeep({ obj: n })).toBe(false);
expect(C.testConditionalDeep({ obj: n })).toBe(false);
expect(C.testLoopDeep({ obj: n })).toBe(false);
expect(C.testNegateDeep({ obj: n })).toBe(false);

expect(C.testLogicalInIf(n)).toBe(false);
expect(C.testLogicalInReturn(n)).toBe(undefined);

expect(C.testNullishCoalescing(n)).toBe(n);
}
}
}

C.test();
C.testNullish();