From 788a3c299ec529b853115b38484d165d7c293f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 23 Oct 2023 10:08:20 +0200 Subject: [PATCH 1/2] [babel 8] Inline `toSequenceExpression` into `@babel/traverse` --- .../babel-traverse/src/path/replacement.ts | 88 +++++++++- packages/babel-traverse/test/replacement.js | 164 ++++++++++++++++++ .../converters/gatherSequenceExpressions.ts | 7 + .../src/converters/toSequenceExpression.ts | 7 + packages/babel-types/src/index.ts | 8 +- packages/babel-types/test/converters.js | 116 ------------- 6 files changed, 268 insertions(+), 122 deletions(-) diff --git a/packages/babel-traverse/src/path/replacement.ts b/packages/babel-traverse/src/path/replacement.ts index 3508097fa3da..f2845bba9f32 100644 --- a/packages/babel-traverse/src/path/replacement.ts +++ b/packages/babel-traverse/src/path/replacement.ts @@ -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"; @@ -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(); @@ -327,6 +336,75 @@ export function replaceExpressionWithStatements( return newCallee.get("body.body"); } +function gatherSequenceExpressions( + nodes: ReadonlyArray, + declars: Array, +) { + 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) { this.resync(); diff --git a/packages/babel-traverse/test/replacement.js b/packages/babel-traverse/test/replacement.js index 7b02f0c9bce1..994be4679bd3 100644 --- a/packages/babel-traverse/test/replacement.js +++ b/packages/babel-traverse/test/replacement.js @@ -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 () { @@ -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; + } + }()" + `); + }); + }); + }); }); diff --git a/packages/babel-types/src/converters/gatherSequenceExpressions.ts b/packages/babel-types/src/converters/gatherSequenceExpressions.ts index 9b7b39b6227a..3e126db6aa67 100644 --- a/packages/babel-types/src/converters/gatherSequenceExpressions.ts +++ b/packages/babel-types/src/converters/gatherSequenceExpressions.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 getBindingIdentifiers from "../retrievers/getBindingIdentifiers.ts"; import { isExpression, diff --git a/packages/babel-types/src/converters/toSequenceExpression.ts b/packages/babel-types/src/converters/toSequenceExpression.ts index bfd6a302813a..7192bc03c6c0 100644 --- a/packages/babel-types/src/converters/toSequenceExpression.ts +++ b/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"; diff --git a/packages/babel-types/src/index.ts b/packages/babel-types/src/index.ts index bdd06fad6b24..deeaa5455f86 100644 --- a/packages/babel-types/src/index.ts +++ b/packages/babel-types/src/index.ts @@ -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"; @@ -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) { + // eslint-disable-next-line no-restricted-globals + exports.toSequenceExpression = + // eslint-disable-next-line no-restricted-globals + require("./converters/toSequenceExpression.js").default; +} diff --git a/packages/babel-types/test/converters.js b/packages/babel-types/test/converters.js index f616c749974b..cfc392c99431 100644 --- a/packages/babel-types/test/converters.js +++ b/packages/babel-types/test/converters.js @@ -1,17 +1,4 @@ import * as t from "../lib/index.js"; -import { parse } from "@babel/parser"; - -import _generate from "@babel/generator"; -const generate = _generate.default || _generate; - -function parseCode(string) { - return parse(string, { - allowReturnOutsideFunction: true, - }).program.body[0]; -} -function generateCode(node) { - return generate(node).code; -} describe("converters", function () { it("toIdentifier", function () { @@ -231,107 +218,4 @@ describe("converters", function () { t.assertProgram(node); }); }); - describe("toSequenceExpression", function () { - let scope; - const undefinedNode = t.identifier("undefined"); - beforeEach(function () { - scope = []; - scope.buildUndefinedNode = function () { - return undefinedNode; - }; - }); - it("gathers nodes into sequence", function () { - const node = t.identifier("a"); - const sequence = t.toSequenceExpression([undefinedNode, node], scope); - t.assertSequenceExpression(sequence); - expect(sequence.expressions[0]).toBe(undefinedNode); - expect(sequence.expressions[1]).toBe(node); - t.assertIdentifier(node); - }); - it("avoids sequence for single node", function () { - const node = t.identifier("a"); - let sequence = t.toSequenceExpression([node], scope); - expect(sequence).toBe(node); - - const block = t.blockStatement([t.expressionStatement(node)]); - sequence = t.toSequenceExpression([block], scope); - expect(sequence).toBe(node); - }); - it("gathers expression", function () { - const node = t.identifier("a"); - const sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(sequence.expressions[1]).toBe(node); - }); - it("gathers expression statement", function () { - const node = t.expressionStatement(t.identifier("a")); - const sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(sequence.expressions[1]).toBe(node.expression); - }); - it("gathers var declarations", function () { - const node = parseCode("var a, b = 1;"); - const sequence = t.toSequenceExpression([undefinedNode, node], scope); - t.assertIdentifier(scope[0].id, { name: "a" }); - t.assertIdentifier(scope[1].id, { name: "b" }); - expect(generateCode(sequence.expressions[1])).toBe("b = 1"); - expect(generateCode(sequence.expressions[2])).toBe("undefined"); - }); - it("skips undefined if expression after var declaration", function () { - const node = parseCode("{ var a, b = 1; true }"); - const sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(generateCode(sequence.expressions[1])).toBe("b = 1, true"); - }); - it("bails on let and const declarations", function () { - let node = parseCode("let a, b = 1;"); - let sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(sequence).toBeUndefined(); - - node = parseCode("const b = 1;"); - sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(sequence).toBeUndefined(); - }); - it("gathers if statements", function () { - let node = parseCode("if (true) { true }"); - let sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(generateCode(sequence.expressions[1])).toBe( - "true ? true : undefined", - ); - - node = parseCode("if (true) { true } else { b }"); - sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(generateCode(sequence.expressions[1])).toBe("true ? true : b"); - }); - it("bails in if statements if recurse bails", function () { - let node = parseCode("if (true) { return }"); - let sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(sequence).toBeUndefined(); - - node = parseCode("if (true) { true } else { return }"); - sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(sequence).toBeUndefined(); - }); - it("gathers block statements", function () { - let node = parseCode("{ a }"); - let sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(generateCode(sequence.expressions[1])).toBe("a"); - - node = parseCode("{ a; b; }"); - sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(generateCode(sequence.expressions[1])).toBe("a, b"); - }); - it("bails in block statements if recurse bails", function () { - const node = parseCode("{ return }"); - const sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(sequence).toBeUndefined(); - }); - it("gathers empty statements if first element", function () { - const node = parseCode(";"); - const sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(generateCode(sequence)).toBe("undefined"); - }); - it("skips empty statement if expression afterwards", function () { - const node = parseCode("{ ; true }"); - const sequence = t.toSequenceExpression([undefinedNode, node], scope); - expect(generateCode(sequence.expressions[1])).toBe("true"); - }); - }); }); From e63c02c5dec44f7e61e7e27cb86d4514f8b89053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 23 Oct 2023 10:27:02 +0200 Subject: [PATCH 2/2] Fix standalone build --- packages/babel-types/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-types/src/index.ts b/packages/babel-types/src/index.ts index deeaa5455f86..6389359006e5 100644 --- a/packages/babel-types/src/index.ts +++ b/packages/babel-types/src/index.ts @@ -106,7 +106,7 @@ 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) { +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