Skip to content

Commit

Permalink
[babel 8] Inline toSequenceExpression into @babel/traverse (#16057)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Oct 23, 2023
1 parent 46ee461 commit 6d9725c
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 122 deletions.
88 changes: 83 additions & 5 deletions packages/babel-traverse/src/path/replacement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,27 @@ import {
assignmentExpression,
awaitExpression,
blockStatement,
buildUndefinedNode,
callExpression,
cloneNode,
conditionalExpression,
expressionStatement,
getBindingIdentifiers,
identifier,
inheritLeadingComments,
inheritTrailingComments,
inheritsComments,
isBlockStatement,
isEmptyStatement,
isExpression,
isExpressionStatement,
isIfStatement,
isProgram,
isStatement,
isVariableDeclaration,
removeComments,
returnStatement,
toSequenceExpression,
sequenceExpression,
validate,
yieldExpression,
} from "@babel/types";
Expand Down Expand Up @@ -229,10 +237,11 @@ export function replaceExpressionWithStatements(
) {
this.resync();

const nodesAsSequenceExpression = toSequenceExpression(nodes, this.scope);

if (nodesAsSequenceExpression) {
return this.replaceWith(nodesAsSequenceExpression)[0].get("expressions");
const declars: t.Identifier[] = [];
const nodesAsSingleExpression = gatherSequenceExpressions(nodes, declars);
if (nodesAsSingleExpression) {
for (const id of declars) this.scope.push({ id });
return this.replaceWith(nodesAsSingleExpression)[0].get("expressions");
}

const functionParent = this.getFunctionParent();
Expand Down Expand Up @@ -327,6 +336,75 @@ export function replaceExpressionWithStatements(
return newCallee.get("body.body");
}

function gatherSequenceExpressions(
nodes: ReadonlyArray<t.Node>,
declars: Array<t.Identifier>,
) {
const exprs: t.Expression[] = [];
let ensureLastUndefined = true;

for (const node of nodes) {
// if we encounter emptyStatement before a non-emptyStatement
// we want to disregard that
if (!isEmptyStatement(node)) {
ensureLastUndefined = false;
}

if (isExpression(node)) {
exprs.push(node);
} else if (isExpressionStatement(node)) {
exprs.push(node.expression);
} else if (isVariableDeclaration(node)) {
if (node.kind !== "var") return; // bailed

for (const declar of node.declarations) {
const bindings = getBindingIdentifiers(declar);
for (const key of Object.keys(bindings)) {
declars.push(cloneNode(bindings[key]));
}

if (declar.init) {
exprs.push(assignmentExpression("=", declar.id, declar.init));
}
}

ensureLastUndefined = true;
} else if (isIfStatement(node)) {
const consequent = node.consequent
? gatherSequenceExpressions([node.consequent], declars)
: buildUndefinedNode();
const alternate = node.alternate
? gatherSequenceExpressions([node.alternate], declars)
: buildUndefinedNode();
if (!consequent || !alternate) return; // bailed

exprs.push(conditionalExpression(node.test, consequent, alternate));
} else if (isBlockStatement(node)) {
const body = gatherSequenceExpressions(node.body, declars);
if (!body) return; // bailed

exprs.push(body);
} else if (isEmptyStatement(node)) {
// empty statement so ensure the last item is undefined if we're last
// checks if emptyStatement is first
if (nodes.indexOf(node) === 0) {
ensureLastUndefined = true;
}
} else {
// bailed, we can't turn this statement into an expression
return;
}
}

if (ensureLastUndefined) exprs.push(buildUndefinedNode());

if (exprs.length === 1) {
return exprs[0];
} else {
return sequenceExpression(exprs);
}
}

export function replaceInline(this: NodePath, nodes: t.Node | Array<t.Node>) {
this.resync();

Expand Down
164 changes: 164 additions & 0 deletions packages/babel-traverse/test/replacement.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import _generate from "@babel/generator";
const traverse = _traverse.default || _traverse;
const generate = _generate.default || _generate;

function getPath(code) {
const ast = parse(code);
let path;
traverse(ast, {
Program: function (_path) {
path = _path.get("body.0");
_path.stop();
},
});

return path;
}

describe("path/replacement", function () {
describe("replaceWith", function () {
it("replaces declaration in ExportDefaultDeclaration node", function () {
Expand Down Expand Up @@ -188,4 +201,155 @@ describe("path/replacement", function () {
expect(visitCounter).toBe(1);
});
});
describe("replaceExpressionWithStatements", function () {
const undefinedNode = t.expressionStatement(t.identifier("undefined"));

const getExprPath = () => getPath("X;").get("expression");
const parseStmt = code =>
parse(code, { allowReturnOutsideFunction: true }).program.body[0];

it("gathers nodes into sequence", function () {
const path = getExprPath();
const node = t.identifier("a");
path.replaceExpressionWithStatements([undefinedNode, node]);
t.assertSequenceExpression(path.node);
expect(path.node.expressions[0]).toBe(undefinedNode.expression);
expect(path.node.expressions[1]).toBe(node);
});
it("avoids sequence for single node", function () {
const path = getExprPath();

const node = t.identifier("a");
path.replaceExpressionWithStatements([node]);
expect(path.node).toBe(node);

const block = t.blockStatement([t.expressionStatement(node)]);
path.replaceExpressionWithStatements([block]);
expect(path.node).toBe(node);
});
it("gathers expression", function () {
const path = getExprPath();
const node = t.identifier("a");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.node.expressions[1]).toBe(node);
});
it("gathers expression statement", function () {
const path = getExprPath();
const node = t.expressionStatement(t.identifier("a"));
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.node.expressions[1]).toBe(node.expression);
});
it("gathers var declarations", function () {
const path = getExprPath();
const node = parseStmt("var a, b = 1;");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.scope.hasOwnBinding("a")).toBe(true);
expect(path.scope.hasOwnBinding("b")).toBe(true);
expect(path.get("expressions.0").toString()).toBe("undefined");
expect(path.get("expressions.1").toString()).toBe("b = 1");
expect(path.get("expressions.2").toString()).toBe("void 0");
});
it("skips undefined if expression after var declaration", function () {
const path = getExprPath();
const node = parseStmt("{ var a, b = 1; true }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.get("expressions.1").toString()).toBe("b = 1, true");
});
it("bails on let and const declarations", function () {
let path = getExprPath();

let node = parseStmt("let a, b = 1;");
path.replaceExpressionWithStatements([undefinedNode, node]);
t.assertCallExpression(path.node);
t.assertFunction(path.node.callee);

path = getExprPath();
node = parseStmt("const b = 1;");
path.replaceExpressionWithStatements([undefinedNode, node]);
t.assertCallExpression(path.node);
t.assertFunction(path.node.callee);
});
it("gathers if statements", function () {
let path = getExprPath();
let node = parseStmt("if (c) { true }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.get("expressions.1").toString()).toBe("c ? true : void 0");

path = getExprPath();
node = parseStmt("if (c) { true } else { b }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.get("expressions.1").toString()).toBe("c ? true : b");
});
it("gathers block statements", function () {
let path = getExprPath();
let node = parseStmt("{ a }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.get("expressions.1").toString()).toBe("a");

path = getExprPath();
node = parseStmt("{ a; b; }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.get("expressions.1").toString()).toBe("a, b");
});
it("gathers empty statements if first element", function () {
const path = getExprPath();
const node = parseStmt(";");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.toString()).toBe("undefined");
});
it("skips empty statement if expression afterwards", function () {
const path = getExprPath();
const node = parseStmt("{ ; true }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.get("expressions.1").toString()).toBe("true");
});
describe("return", function () {
// TODO: These tests veryfy wrong behavior. It's not possible to
// replace an expression with `return`, as wrapping it in a IIFE changes
// semantics.
// They are here because it's how @babel/traverse currently behaves, but
// it should be eventually be made to throw an error.

it("bails in if statements if recurse bails", function () {
let path = getExprPath();
let node = parseStmt("if (true) { return }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.toString()).toMatchInlineSnapshot(`
"function () {
undefined;
if (true) {
return;
}
}()"
`);

path = getExprPath();
node = parseStmt("if (true) { true } else { return }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.toString()).toMatchInlineSnapshot(`
"function () {
undefined;
if (true) {
return true;
} else {
return;
}
}()"
`);
});
it("bails in block statements if recurse bails", function () {
const path = getExprPath();
const node = parseStmt("{ return }");
path.replaceExpressionWithStatements([undefinedNode, node]);
expect(path.toString()).toMatchInlineSnapshot(`
"function () {
undefined;
{
return;
}
}()"
`);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
// TODO(Babel 8) Remove this file
if (process.env.BABEL_8_BREAKING) {
throw new Error(
"Internal Babel error: This file should only be loaded in Babel 7",
);
}

import getBindingIdentifiers from "../retrievers/getBindingIdentifiers.ts";
import {
isExpression,
Expand Down
7 changes: 7 additions & 0 deletions packages/babel-types/src/converters/toSequenceExpression.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
// TODO(Babel 8) Remove this file
if (process.env.BABEL_8_BREAKING) {
throw new Error(
"Internal Babel error: This file should only be loaded in Babel 7",
);
}

import gatherSequenceExpressions from "./gatherSequenceExpressions.ts";
import type * as t from "../index.ts";
import type { DeclarationInfo } from "./gatherSequenceExpressions.ts";
Expand Down
8 changes: 7 additions & 1 deletion packages/babel-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export { default as toComputedKey } from "./converters/toComputedKey.ts";
export { default as toExpression } from "./converters/toExpression.ts";
export { default as toIdentifier } from "./converters/toIdentifier.ts";
export { default as toKeyAlias } from "./converters/toKeyAlias.ts";
export { default as toSequenceExpression } from "./converters/toSequenceExpression.ts";
export { default as toStatement } from "./converters/toStatement.ts";
export { default as valueToNode } from "./converters/valueToNode.ts";

Expand Down Expand Up @@ -106,3 +105,10 @@ export type * from "./ast-types/generated/index.ts";

// this is used by @babel/traverse to warn about deprecated visitors
export { default as __internal__deprecationWarning } from "./utils/deprecationWarning.ts";

if (!process.env.BABEL_8_BREAKING && !USE_ESM && !IS_STANDALONE) {
// eslint-disable-next-line no-restricted-globals
exports.toSequenceExpression =
// eslint-disable-next-line no-restricted-globals
require("./converters/toSequenceExpression.js").default;
}

0 comments on commit 6d9725c

Please sign in to comment.