diff --git a/packages/babel-types/package.json b/packages/babel-types/package.json index 73f343ab0785..0d4caf54310c 100644 --- a/packages/babel-types/package.json +++ b/packages/babel-types/package.json @@ -13,6 +13,7 @@ "to-fast-properties": "^1.0.1" }, "devDependencies": { + "babel-generator": "7.0.0-alpha.9", "babylon": "^7.0.0-beta.8" } } diff --git a/packages/babel-types/src/converters.js b/packages/babel-types/src/converters.js index b6510c88d5f8..a76d6bd776b8 100644 --- a/packages/babel-types/src/converters.js +++ b/packages/babel-types/src/converters.js @@ -10,6 +10,71 @@ export function toComputedKey(node: Object, key: Object = node.key || node.prope return key; } + +function gatherSequenceExpressions(nodes: Array, scope: Scope, declars: Array): ?Object { + const exprs = []; + let ensureLastUndefined = true; + + for (const node of nodes) { + ensureLastUndefined = false; + + if (t.isExpression(node)) { + exprs.push(node); + } else if (t.isExpressionStatement(node)) { + exprs.push(node.expression); + } else if (t.isVariableDeclaration(node)) { + if (node.kind !== "var") return; // bailed + + for (const declar of (node.declarations: Array)) { + const bindings = t.getBindingIdentifiers(declar); + for (const key in bindings) { + declars.push({ + kind: node.kind, + id: bindings[key], + }); + } + + if (declar.init) { + exprs.push(t.assignmentExpression("=", declar.id, declar.init)); + } + } + + ensureLastUndefined = true; + } else if (t.isIfStatement(node)) { + const consequent = node.consequent ? + gatherSequenceExpressions([node.consequent], scope, declars) : + scope.buildUndefinedNode(); + const alternate = node.alternate ? + gatherSequenceExpressions([node.alternate], scope, declars) : + scope.buildUndefinedNode(); + if (!consequent || !alternate) return; // bailed + + exprs.push(t.conditionalExpression(node.test, consequent, alternate)); + } else if (t.isBlockStatement(node)) { + const body = gatherSequenceExpressions(node.body, scope, declars); + if (!body) return; // bailed + + exprs.push(body); + } else if (t.isEmptyStatement(node)) { + // empty statement so ensure the last item is undefined if we're last + ensureLastUndefined = true; + } else { + // bailed, we can't turn this statement into an expression + return; + } + } + + if (ensureLastUndefined) { + exprs.push(scope.buildUndefinedNode()); + } + + if (exprs.length === 1) { + return exprs[0]; + } else { + return t.sequenceExpression(exprs); + } +} + /** * Turn an array of statement `nodes` into a `SequenceExpression`. * @@ -23,80 +88,14 @@ export function toSequenceExpression(nodes: Array, scope: Scope): ?Objec if (!nodes || !nodes.length) return; const declars = []; - let bailed = false; - - const result = convert(nodes); - if (bailed) return; + const result = gatherSequenceExpressions(nodes, scope, declars); + if (!result) return; - for (let i = 0; i < declars.length; i++) { - scope.push(declars[i]); + for (const declar of declars) { + scope.push(declar); } return result; - - function convert(nodes) { - let ensureLastUndefined = false; - const exprs = []; - - for (const node of (nodes: Array)) { - if (t.isExpression(node)) { - exprs.push(node); - } else if (t.isExpressionStatement(node)) { - exprs.push(node.expression); - } else if (t.isVariableDeclaration(node)) { - if (node.kind !== "var") return bailed = true; // bailed - - for (const declar of (node.declarations: Array)) { - const bindings = t.getBindingIdentifiers(declar); - for (const key in bindings) { - declars.push({ - kind: node.kind, - id: bindings[key], - }); - } - - if (declar.init) { - exprs.push(t.assignmentExpression("=", declar.id, declar.init)); - } - } - - ensureLastUndefined = true; - continue; - } else if (t.isIfStatement(node)) { - const consequent = node.consequent ? convert([node.consequent]) : scope.buildUndefinedNode(); - const alternate = node.alternate ? convert([node.alternate]) : scope.buildUndefinedNode(); - - if (consequent === true || alternate === true) { - return bailed = true; - } - - exprs.push(t.conditionalExpression(node.test, consequent, alternate)); - } else if (t.isBlockStatement(node)) { - exprs.push(convert(node.body)); - } else if (t.isEmptyStatement(node)) { - // empty statement so ensure the last item is undefined if we're last - ensureLastUndefined = true; - continue; - } else { - // bailed, we can't turn this statement into an expression - return bailed = true; - } - - ensureLastUndefined = false; - } - - if (ensureLastUndefined || exprs.length === 0) { - exprs.push(scope.buildUndefinedNode()); - } - - // - - if (exprs.length === 1) { - return exprs[0]; - } else { - return t.sequenceExpression(exprs); - } - } } export function toKeyAlias(node: Object, key: Object = node.key): string { diff --git a/packages/babel-types/test/converters.js b/packages/babel-types/test/converters.js index 43e1e887a7e2..31584eb6e40b 100644 --- a/packages/babel-types/test/converters.js +++ b/packages/babel-types/test/converters.js @@ -1,5 +1,16 @@ import * as t from "../lib"; import { assert } from "chai"; +import { parse } from "babylon"; +import generate from "babel-generator"; + +function parseCode(string) { + return parse(string, { + allowReturnOutsideFunction: true, + }).program.body[0]; +} +function generateCode(node) { + return generate(node).code; +} describe("converters", function () { it("toIdentifier", function () { @@ -149,4 +160,105 @@ 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); + assert.equal(sequence.expressions[0], undefinedNode); + assert.equal(sequence.expressions[1], node); + t.assertIdentifier(node); + }); + it("avoids sequence for single node", function () { + const node = t.identifier("a"); + let sequence = t.toSequenceExpression([node], scope); + assert.equal(sequence, node); + + const block = t.blockStatement([t.expressionStatement(node)]); + sequence = t.toSequenceExpression([block], scope); + assert.equal(sequence, node); + }); + it("gathers expression", function () { + const node = t.identifier("a"); + const sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.equal(sequence.expressions[1], node); + }); + it("gathers expression statement", function () { + const node = t.expressionStatement(t.identifier("a")); + const sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.equal(sequence.expressions[1], 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" }); + assert.equal(generateCode(sequence.expressions[1]), "b = 1"); + assert.equal(generateCode(sequence.expressions[2]), "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); + assert.equal(generateCode(sequence.expressions[1]), "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); + assert.isUndefined(sequence); + + node = parseCode("const b = 1;"); + sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.isUndefined(sequence); + }); + it("gathers if statements", function () { + let node = parseCode("if (true) { true }"); + let sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.equal(generateCode(sequence.expressions[1]), "true ? true : undefined"); + + node = parseCode("if (true) { true } else { b }"); + sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.equal(generateCode(sequence.expressions[1]), "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); + assert.isUndefined(sequence); + + node = parseCode("if (true) { true } else { return }"); + sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.isUndefined(sequence); + }); + it("gathers block statements", function () { + let node = parseCode("{ a }"); + let sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.equal(generateCode(sequence.expressions[1]), "a"); + + node = parseCode("{ a; b; }"); + sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.equal(generateCode(sequence.expressions[1]), "a, b"); + }); + it("bails in block statements if recurse bails", function () { + const node = parseCode("{ return }"); + const sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.isUndefined(sequence); + }); + it("gathers empty statements", function () { + const node = parseCode(";"); + const sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.equal(generateCode(sequence.expressions[1]), "undefined"); + }); + it("skips empty statement if expression afterwards", function () { + const node = parseCode("{ ; true }"); + const sequence = t.toSequenceExpression([undefinedNode, node], scope); + assert.equal(generateCode(sequence.expressions[1]), "true"); + }); + }); });