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

[babel 8] Inline toSequenceExpression into @babel/traverse #16057

Merged
merged 2 commits into from Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 83 additions & 5 deletions packages/babel-traverse/src/path/replacement.ts
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
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;
}
}()"
`);
});
});
});
});
@@ -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
@@ -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
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;
}