"use strict"; const REPLACED = Symbol("replaced"); const h = require("./helpers"); module.exports = t => { function mergeNestedIfs(path) { const consequent = path.get("consequent"); const alternate = path.get("alternate"); // not nested if if (!consequent.isIfStatement()) return; // there are no alternate nodes in both the if statements (nested) if (alternate.node || consequent.get("alternate").node) return; const test = path.get("test"); test.replaceWith(t.logicalExpression("&&", test.node, consequent.get("test").node)); consequent.replaceWith(t.clone(consequent.get("consequent").node)); } // No alternate, make into a guarded expression function toGuardedExpression(path) { const node = path.node; if (node.consequent && !node.alternate && node.consequent.type === "ExpressionStatement") { let op = "&&"; if (t.isUnaryExpression(node.test, { operator: "!" })) { node.test = node.test.argument; op = "||"; } path.replaceWith(t.expressionStatement(t.logicalExpression(op, node.test, node.consequent.expression))); return REPLACED; } } // both consequent and alternate are expressions, turn into ternary function toTernary(path) { const node = path.node; if (t.isExpressionStatement(node.consequent) && t.isExpressionStatement(node.alternate)) { path.replaceWith(t.conditionalExpression(node.test, node.consequent.expression, node.alternate.expression)); return REPLACED; } } // consequent and alternate are return -- conditional. function toConditional(path) { const node = path.node; if (t.isReturnStatement(node.consequent) && t.isReturnStatement(node.alternate)) { if (!node.consequent.argument && !node.alternate.argument) { path.replaceWith(t.expressionStatement(node.test)); return REPLACED; } path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument || h.VOID_0(t), node.alternate.argument || h.VOID_0(t)))); return REPLACED; } } // There is nothing after this If block. And one or both // of the consequent and alternate are either expression statment // or return statements. function toReturn(path) { const node = path.node; if (!path.getSibling(path.key + 1).node && path.parentPath && path.parentPath.parentPath && path.parentPath.parentPath.isFunction()) { // Only the consequent is a return, void the alternate. if (t.isReturnStatement(node.consequent) && t.isExpressionStatement(node.alternate)) { if (!node.consequent.argument) { path.replaceWith(t.expressionStatement(t.logicalExpression("||", node.test, node.alternate.expression))); return REPLACED; } path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument || h.VOID_0(t), t.unaryExpression("void", node.alternate.expression, true)))); return REPLACED; } // Only the alternate is a return, void the consequent. if (t.isReturnStatement(node.alternate) && t.isExpressionStatement(node.consequent)) { if (!node.alternate.argument) { path.replaceWith(t.expressionStatement(t.logicalExpression("&&", node.test, node.consequent.expression))); return REPLACED; } path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, t.unaryExpression("void", node.consequent.expression, true), node.alternate.argument || h.VOID_0(t)))); return REPLACED; } if (t.isReturnStatement(node.consequent) && !node.alternate) { if (!node.consequent.argument) { path.replaceWith(t.expressionStatement(node.test)); return REPLACED; } // This would only be worth it if the previous statement was an if // because then we may merge to create a conditional. if (path.getSibling(path.key - 1).isIfStatement()) { path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument || h.VOID_0(t), h.VOID_0(t)))); return REPLACED; } } if (t.isReturnStatement(node.alternate) && !node.consequent) { if (!node.alternate.argument) { path.replaceWith(t.expressionStatement(node.test)); return REPLACED; } // Same as above. if (path.getSibling(path.key - 1).isIfStatement()) { path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.alternate.argument || h.VOID_0(t), h.VOID_0(t)))); return REPLACED; } } } let next = path.getSibling(path.key + 1); // If the next satatement(s) is an if statement and we can simplify that // to potentailly be an expression (or a return) then this will make it // easier merge. if (next.isIfStatement()) { next.pushContext(path.context); next.visit(); next.popContext(); next = path.getSibling(path.key + 1); } // Some other visitor might have deleted our node. OUR NODE ;_; if (!path.node) { return; } // No alternate but the next statement is a return // also turn into a return conditional if (t.isReturnStatement(node.consequent) && !node.alternate && next.isReturnStatement()) { const nextArg = next.node.argument || h.VOID_0(t); next.remove(); path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument || h.VOID_0(t), nextArg))); return REPLACED; } // Next is the last expression, turn into a return while void'ing the exprs if (path.parentPath && path.parentPath.parentPath && path.parentPath.parentPath.isFunction() && !path.getSibling(path.key + 2).node && t.isReturnStatement(node.consequent) && !node.alternate && next.isExpressionStatement()) { const nextExpr = next.node.expression; next.remove(); if (node.consequent.argument) { path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument, t.unaryExpression("void", nextExpr, true)))); return REPLACED; } path.replaceWith(t.logicalExpression("||", node.test, nextExpr)); return REPLACED; } } // Remove else for if-return function removeUnnecessaryElse(path) { const node = path.node; const consequent = path.get("consequent"); const alternate = path.get("alternate"); if (consequent.node && alternate.node && (consequent.isReturnStatement() || consequent.isBlockStatement() && t.isReturnStatement(consequent.node.body[consequent.node.body.length - 1])) && ( // don't hoist declarations // TODO: validate declarations after fixing scope issues alternate.isBlockStatement() ? !alternate.get("body").some(stmt => stmt.isVariableDeclaration({ kind: "let" }) || stmt.isVariableDeclaration({ kind: "const" })) : true)) { path.insertAfter(alternate.isBlockStatement() ? alternate.node.body.map(el => t.clone(el)) : t.clone(alternate.node)); node.alternate = null; return REPLACED; } } function runTransforms(path) { // ordered const transforms = [toGuardedExpression, toTernary, toConditional, toReturn, removeUnnecessaryElse]; // run each of the replacement till we replace something // which is identified by the Symbol(REPLACED) that each of the // functions return when they replace something for (var _i = 0; _i < transforms.length; _i++) { const transform = transforms[_i]; if (transform(path) === REPLACED) { break; } } } // If the consequent is if and the altenrate is not then // switch them out. That way we know we don't have to print // a block.x function switchConsequent(path) { const node = path.node; if (!node.alternate) { return; } if (!t.isIfStatement(node.consequent)) { return; } if (t.isIfStatement(node.alternate)) { return; } node.test = t.unaryExpression("!", node.test, true); var _ref = [node.consequent, node.alternate]; node.alternate = _ref[0]; node.consequent = _ref[1]; } // Make if statements with conditional returns in the body into // an if statement that guards the rest of the block. function conditionalReturnToGuards(path) { const node = path.node; if (!path.inList || !path.get("consequent").isBlockStatement() || node.alternate) { return; } let ret; let test; const exprs = []; const statements = node.consequent.body; for (let i = 0, statement; statement = statements[i]; i++) { if (t.isExpressionStatement(statement)) { exprs.push(statement.expression); } else if (t.isIfStatement(statement)) { if (i < statements.length - 1) { // This isn't the last statement. Bail. return; } if (statement.alternate) { return; } if (!t.isReturnStatement(statement.consequent)) { return; } ret = statement.consequent; test = statement.test; } else { return; } } if (!test || !ret) { return; } exprs.push(test); const expr = exprs.length === 1 ? exprs[0] : t.sequenceExpression(exprs); const replacement = t.logicalExpression("&&", node.test, expr); path.replaceWith(t.ifStatement(replacement, ret, null)); } return { mergeNestedIfs, simplify: runTransforms, switchConsequent, conditionalReturnToGuards }; };