From 25e57f246a41bdb8b33bfe969016583945801d7b Mon Sep 17 00:00:00 2001 From: Boopathi Rajaa Date: Mon, 10 Jul 2017 19:57:42 +0200 Subject: [PATCH] Split simplify plugin to multiple files (#624) * Split Simplify to multiple files - IfStatement * Extract Conditional Expression transformations to separate file * Move Logical Expression patterns to separate file * Move AssignmentExpression to separate file --- .../src/assignment-expression.js | 125 ++++ .../src/conditional-expression.js | 53 ++ .../src/helpers.js | 51 ++ .../src/if-statement.js | 391 +++++++++++ .../babel-plugin-minify-simplify/src/index.js | 634 +----------------- .../src/logical-expression.js | 79 +++ 6 files changed, 710 insertions(+), 623 deletions(-) create mode 100644 packages/babel-plugin-minify-simplify/src/assignment-expression.js create mode 100644 packages/babel-plugin-minify-simplify/src/conditional-expression.js create mode 100644 packages/babel-plugin-minify-simplify/src/helpers.js create mode 100644 packages/babel-plugin-minify-simplify/src/if-statement.js create mode 100644 packages/babel-plugin-minify-simplify/src/logical-expression.js diff --git a/packages/babel-plugin-minify-simplify/src/assignment-expression.js b/packages/babel-plugin-minify-simplify/src/assignment-expression.js new file mode 100644 index 000000000..ec8834faa --- /dev/null +++ b/packages/babel-plugin-minify-simplify/src/assignment-expression.js @@ -0,0 +1,125 @@ +"use strict"; + +const operators = new Set([ + "+", + "-", + "*", + "%", + "<<", + ">>", + ">>>", + "&", + "|", + "^", + "/", + "**" +]); + +const updateOperators = new Set(["+", "-"]); + +module.exports = t => { + function simplify(path) { + const rightExpr = path.get("right"); + const leftExpr = path.get("left"); + + if (path.node.operator !== "=") { + return; + } + + const canBeUpdateExpression = + rightExpr.get("right").isNumericLiteral() && + rightExpr.get("right").node.value === 1 && + updateOperators.has(rightExpr.node.operator); + + if (leftExpr.isMemberExpression()) { + const leftPropNames = getPropNames(leftExpr); + const rightPropNames = getPropNames(rightExpr.get("left")); + + if ( + !leftPropNames || + leftPropNames.indexOf(undefined) > -1 || + !rightPropNames || + rightPropNames.indexOf(undefined) > -1 || + !operators.has(rightExpr.node.operator) || + !areArraysEqual(leftPropNames, rightPropNames) + ) { + return; + } + } else { + if ( + !rightExpr.isBinaryExpression() || + !operators.has(rightExpr.node.operator) || + leftExpr.node.name !== rightExpr.node.left.name + ) { + return; + } + } + + let newExpression; + + // special case x=x+1 --> ++x + if (canBeUpdateExpression) { + newExpression = t.updateExpression( + rightExpr.node.operator + rightExpr.node.operator, + t.clone(leftExpr.node), + true /* prefix */ + ); + } else { + newExpression = t.assignmentExpression( + rightExpr.node.operator + "=", + t.clone(leftExpr.node), + t.clone(rightExpr.node.right) + ); + } + + path.replaceWith(newExpression); + } + + return { + simplify + }; +}; + +function areArraysEqual(arr1, arr2) { + return arr1.every((value, index) => { + return String(value) === String(arr2[index]); + }); +} + +function getPropNames(path) { + if (!path.isMemberExpression()) { + return; + } + + let obj = path.get("object"); + + const prop = path.get("property"); + const propNames = [getName(prop.node)]; + + while (obj.type === "MemberExpression") { + const node = obj.get("property").node; + if (node) { + propNames.push(getName(node)); + } + obj = obj.get("object"); + } + propNames.push(getName(obj.node)); + + return propNames; +} + +function getName(node) { + if (node.type === "ThisExpression") { + return "this"; + } + if (node.type === "Super") { + return "super"; + } + if (node.type === "NullLiteral") { + return "null"; + } + // augment identifiers so that they don't match + // string/number literals + // but still match against each other + return node.name ? node.name + "_" : node.value /* Literal */; +} diff --git a/packages/babel-plugin-minify-simplify/src/conditional-expression.js b/packages/babel-plugin-minify-simplify/src/conditional-expression.js new file mode 100644 index 000000000..deb980e17 --- /dev/null +++ b/packages/babel-plugin-minify-simplify/src/conditional-expression.js @@ -0,0 +1,53 @@ +const h = require("./helpers"); +const PatternMatch = require("./pattern-match"); + +module.exports = t => { + // small abstractions + const not = node => t.unaryExpression("!", node); + const notnot = node => not(not(node)); + const or = (a, b) => t.logicalExpression("||", a, b); + const and = (a, b) => t.logicalExpression("&&", a, b); + + function simplifyPatterns(path) { + const test = path.get("test"); + const consequent = path.get("consequent"); + const alternate = path.get("alternate"); + + const { Expression: EX, LogicalExpression: LE } = h.typeSymbols(t); + + // Convention: + // =============== + // for each pattern [test, consequent, alternate, handler(expr, cons, alt)] + const matcher = new PatternMatch([ + [LE, true, false, e => e], + [EX, true, false, e => notnot(e)], + + [EX, false, true, e => not(e)], + + [LE, true, EX, (e, c, a) => or(e, a)], + [EX, true, EX, (e, c, a) => or(notnot(e), a)], + + [EX, false, EX, (e, c, a) => and(not(e), a)], + + [EX, EX, true, (e, c) => or(not(e), c)], + + [LE, EX, false, (e, c) => and(e, c)], + [EX, EX, false, (e, c) => and(notnot(e), c)] + ]); + + const result = matcher.match( + [test, consequent, alternate], + h.isPatternMatchesPath(t) + ); + + if (result.match) { + path.replaceWith( + result.value(test.node, consequent.node, alternate.node) + ); + } + } + + return { + simplifyPatterns + }; +}; diff --git a/packages/babel-plugin-minify-simplify/src/helpers.js b/packages/babel-plugin-minify-simplify/src/helpers.js new file mode 100644 index 000000000..a3bad07b4 --- /dev/null +++ b/packages/babel-plugin-minify-simplify/src/helpers.js @@ -0,0 +1,51 @@ +"use strict"; + +const VOID_0 = t => t.unaryExpression("void", t.numericLiteral(0), true); + +// Types as Symbols - for comparing types +// init must be empty object - +// computing this involves checking object.keys() to be of length 0 +// skipped otherwise +const types = {}; +const typeSymbols = t => { + // don't recompute + if (Object.keys(types).length < 1) { + t.TYPES.forEach(type => { + types[type] = Symbol.for(type); + }); + } + return types; +}; + +const isNodeOfType = (t, node, typeSymbol) => + typeof typeSymbol !== "symbol" + ? false + : t["is" + Symbol.keyFor(typeSymbol)](node); + +const isPatternMatchesPath = t => + function _isPatternMatchesPath(patternValue, inputPath) { + if (Array.isArray(patternValue)) { + for (let i = 0; i < patternValue.length; i++) { + if (_isPatternMatchesPath(patternValue[i], inputPath)) { + return true; + } + } + return false; + } + if (typeof patternValue === "function") { + return patternValue(inputPath); + } + if (isNodeOfType(t, inputPath.node, patternValue)) return true; + const evalResult = inputPath.evaluate(); + if (!evalResult.confident || !inputPath.isPure()) return false; + return evalResult.value === patternValue; + }; + +module.exports = { + VOID_0, + // Types as Symbols + typeSymbols, + // This is required for resolving type aliases + isNodeOfType, + isPatternMatchesPath +}; diff --git a/packages/babel-plugin-minify-simplify/src/if-statement.js b/packages/babel-plugin-minify-simplify/src/if-statement.js new file mode 100644 index 000000000..37cefc1af --- /dev/null +++ b/packages/babel-plugin-minify-simplify/src/if-statement.js @@ -0,0 +1,391 @@ +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; + 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; + 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; + 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; + + 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; + 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] + ))) + ) { + 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 (const transform of transforms) { + 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; + + if (!node.alternate) { + return; + } + + if (!t.isIfStatement(node.consequent)) { + return; + } + + if (t.isIfStatement(node.alternate)) { + return; + } + + node.test = t.unaryExpression("!", node.test, true); + [node.alternate, node.consequent] = [node.consequent, node.alternate]; + } + + // 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; + + 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 + }; +}; diff --git a/packages/babel-plugin-minify-simplify/src/index.js b/packages/babel-plugin-minify-simplify/src/index.js index fbf3ae45b..9e281cff7 100644 --- a/packages/babel-plugin-minify-simplify/src/index.js +++ b/packages/babel-plugin-minify-simplify/src/index.js @@ -1,97 +1,20 @@ "use strict"; -const PatternMatch = require("./pattern-match"); - module.exports = ({ types: t }) => { const flipExpressions = require("babel-helper-flip-expressions")(t); const toMultipleSequenceExpressions = require("babel-helper-to-multiple-sequence-expressions")( t ); + const ifStatement = require("./if-statement")(t); + const conditionalExpression = require("./conditional-expression")(t); + const logicalExpression = require("./logical-expression")(t); + const assignmentExpression = require("./assignment-expression")(t); const VOID_0 = t.unaryExpression("void", t.numericLiteral(0), true); const condExprSeen = Symbol("condExprSeen"); const seqExprSeen = Symbol("seqExprSeen"); const shouldRevisit = Symbol("shouldRevisit"); - // Types as symbols for comparisions - const types = {}; - t.TYPES.forEach(type => { - types[type] = Symbol.for(type); - }); - const isNodeOfType = (node, typeSymbol) => { - if (typeof typeSymbol !== "symbol") return false; - return t["is" + Symbol.keyFor(typeSymbol)](node); - }; - - // small abstractions - const not = node => t.unaryExpression("!", node); - const notnot = node => not(not(node)); - const or = (a, b) => t.logicalExpression("||", a, b); - const and = (a, b) => t.logicalExpression("&&", a, b); - - const operators = new Set([ - "+", - "-", - "*", - "%", - "<<", - ">>", - ">>>", - "&", - "|", - "^", - "/", - "**" - ]); - - const updateOperators = new Set(["+", "-"]); - - function areArraysEqual(arr1, arr2) { - return arr1.every((value, index) => { - return String(value) === String(arr2[index]); - }); - } - - function getName(node) { - if (node.type === "ThisExpression") { - return "this"; - } - if (node.type === "Super") { - return "super"; - } - if (node.type === "NullLiteral") { - return "null"; - } - // augment identifiers so that they don't match - // string/number literals - // but still match against each other - return node.name ? node.name + "_" : node.value /* Literal */; - } - - function getPropNames(path) { - if (!path.isMemberExpression()) { - return; - } - - let obj = path.get("object"); - - const prop = path.get("property"); - const propNames = [getName(prop.node)]; - - while (obj.type === "MemberExpression") { - const node = obj.get("property").node; - if (node) { - propNames.push(getName(node)); - } - obj = obj.get("object"); - } - propNames.push(getName(obj.node)); - - return propNames; - } - const OP_AND = input => input === "&&"; - const OP_OR = input => input === "||"; - return { name: "minify-simplify", visitor: { @@ -197,135 +120,10 @@ module.exports = ({ types: t }) => { }, LogicalExpression: { - exit(path) { - // cache of path.evaluate() - const evaluateMemo = new Map(); - - const TRUTHY = input => { - // !NaN and !undefined are truthy - // separate check here as they are considered impure by babel - if ( - input.isUnaryExpression() && - input.get("argument").isIdentifier() - ) { - if ( - input.node.argument.name === "NaN" || - input.node.argument.name === "undefined" - ) { - return true; - } - } - const evalResult = input.evaluate(); - evaluateMemo.set(input, evalResult); - return evalResult.confident && input.isPure() && evalResult.value; - }; - - const FALSY = input => { - // NaN and undefined are falsy - // separate check here as they are considered impure by babel - if (input.isIdentifier()) { - if ( - input.node.name === "NaN" || - input.node.name === "undefined" - ) { - return true; - } - } - const evalResult = input.evaluate(); - evaluateMemo.set(input, evalResult); - return evalResult.confident && input.isPure() && !evalResult.value; - }; - - const { Expression: EX } = types; - - // Convention: - // [left, operator, right, handler(leftNode, rightNode)] - const matcher = new PatternMatch([ - [TRUTHY, OP_AND, EX, (l, r) => r], - [FALSY, OP_AND, EX, l => l], - [TRUTHY, OP_OR, EX, l => l], - [FALSY, OP_OR, EX, (l, r) => r] - ]); - - const left = path.get("left"); - const right = path.get("right"); - const operator = path.node.operator; - - const result = matcher.match( - [left, operator, right], - isPatternMatchesPath - ); - - if (result.match) { - // here we are sure that left.evaluate is always confident becuase - // it satisfied one of TRUTHY/FALSY paths - let value; - if (evaluateMemo.has(left)) { - value = evaluateMemo.get(left).value; - } else { - value = left.evaluate().value; - } - path.replaceWith(result.value(t.valueToNode(value), right.node)); - } - } + exit: logicalExpression.simplifyPatterns }, - AssignmentExpression(path) { - const rightExpr = path.get("right"); - const leftExpr = path.get("left"); - - if (path.node.operator !== "=") { - return; - } - - const canBeUpdateExpression = - rightExpr.get("right").isNumericLiteral() && - rightExpr.get("right").node.value === 1 && - updateOperators.has(rightExpr.node.operator); - - if (leftExpr.isMemberExpression()) { - const leftPropNames = getPropNames(leftExpr); - const rightPropNames = getPropNames(rightExpr.get("left")); - - if ( - !leftPropNames || - leftPropNames.indexOf(undefined) > -1 || - !rightPropNames || - rightPropNames.indexOf(undefined) > -1 || - !operators.has(rightExpr.node.operator) || - !areArraysEqual(leftPropNames, rightPropNames) - ) { - return; - } - } else { - if ( - !rightExpr.isBinaryExpression() || - !operators.has(rightExpr.node.operator) || - leftExpr.node.name !== rightExpr.node.left.name - ) { - return; - } - } - - let newExpression; - - // special case x=x+1 --> ++x - if (canBeUpdateExpression) { - newExpression = t.updateExpression( - rightExpr.node.operator + rightExpr.node.operator, - t.clone(leftExpr.node), - true /* prefix */ - ); - } else { - newExpression = t.assignmentExpression( - rightExpr.node.operator + "=", - t.clone(leftExpr.node), - t.clone(rightExpr.node.right) - ); - } - - path.replaceWith(newExpression); - }, + AssignmentExpression: assignmentExpression.simplify, ConditionalExpression: { enter: [ @@ -347,44 +145,7 @@ module.exports = ({ types: t }) => { } }, - function simplifyPatterns(path) { - const test = path.get("test"); - const consequent = path.get("consequent"); - const alternate = path.get("alternate"); - - const { Expression: EX, LogicalExpression: LE } = types; - - // Convention: - // =============== - // for each pattern [test, consequent, alternate, handler(expr, cons, alt)] - const matcher = new PatternMatch([ - [LE, true, false, e => e], - [EX, true, false, e => notnot(e)], - - [EX, false, true, e => not(e)], - - [LE, true, EX, (e, c, a) => or(e, a)], - [EX, true, EX, (e, c, a) => or(notnot(e), a)], - - [EX, false, EX, (e, c, a) => and(not(e), a)], - - [EX, EX, true, (e, c) => or(not(e), c)], - - [LE, EX, false, (e, c) => and(e, c)], - [EX, EX, false, (e, c) => and(notnot(e), c)] - ]); - - const result = matcher.match( - [test, consequent, alternate], - isPatternMatchesPath - ); - - if (result.match) { - path.replaceWith( - result.value(test.node, consequent.node, alternate.node) - ); - } - } + conditionalExpression.simplifyPatterns ], exit: [ @@ -856,365 +617,10 @@ module.exports = ({ types: t }) => { // turn blocked ifs into single statements IfStatement: { exit: [ - // Merge nested if statements if possible - function({ node }) { - if (!t.isIfStatement(node.consequent)) { - return; - } - - if (node.alternate || node.consequent.alternate) { - return; - } - - node.test = t.logicalExpression( - "&&", - node.test, - node.consequent.test - ); - node.consequent = node.consequent.consequent; - }, - - function(path) { - const { node } = path; - - // No alternate, make into a guarded expression - 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; - } - - // Easy, both are expressions, turn into ternary - if ( - t.isExpressionStatement(node.consequent) && - t.isExpressionStatement(node.alternate) - ) { - path.replaceWith( - t.conditionalExpression( - node.test, - node.consequent.expression, - node.alternate.expression - ) - ); - return; - } - - // Easy: consequent and alternate are return -- conditional. - if ( - t.isReturnStatement(node.consequent) && - t.isReturnStatement(node.alternate) - ) { - if (!node.consequent.argument && !node.alternate.argument) { - path.replaceWith(t.expressionStatement(node.test)); - return; - } - - path.replaceWith( - t.returnStatement( - t.conditionalExpression( - node.test, - node.consequent.argument || VOID_0, - node.alternate.argument || VOID_0 - ) - ) - ); - return; - } - - // There is nothing after this block. And one or both - // of the consequent and alternate are either expression statment - // or return statements. - 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; - } - - path.replaceWith( - t.returnStatement( - t.conditionalExpression( - node.test, - node.consequent.argument || VOID_0, - t.unaryExpression("void", node.alternate.expression, true) - ) - ) - ); - return; - } - - // 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; - } - - path.replaceWith( - t.returnStatement( - t.conditionalExpression( - node.test, - t.unaryExpression( - "void", - node.consequent.expression, - true - ), - node.alternate.argument || VOID_0 - ) - ) - ); - return; - } - - if (t.isReturnStatement(node.consequent) && !node.alternate) { - if (!node.consequent.argument) { - path.replaceWith(t.expressionStatement(node.test)); - return; - } - - // 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 || VOID_0, - VOID_0 - ) - ) - ); - return; - } - } - - if (t.isReturnStatement(node.alternate) && !node.consequent) { - if (!node.alternate.argument) { - path.replaceWith(t.expressionStatement(node.test)); - return; - } - - // Same as above. - if (path.getSibling(path.key - 1).isIfStatement()) { - path.replaceWith( - t.returnStatement( - t.conditionalExpression( - node.test, - node.alternate.argument || VOID_0, - VOID_0 - ) - ) - ); - return; - } - } - } - - 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 || VOID_0; - next.remove(); - path.replaceWith( - t.returnStatement( - t.conditionalExpression( - node.test, - node.consequent.argument || VOID_0, - nextArg - ) - ) - ); - return; - } - - // 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; - } - - path.replaceWith(t.logicalExpression("||", node.test, nextExpr)); - return; - } - - if ( - node.consequent && - node.alternate && - (t.isReturnStatement(node.consequent) || - (t.isBlockStatement(node.consequent) && - t.isReturnStatement( - node.consequent.body[node.consequent.body.length - 1] - ))) - ) { - path.insertAfter( - t.isBlockStatement(node.alternate) - ? node.alternate.body - : node.alternate - ); - node.alternate = null; - return; - } - }, - - // 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(path) { - const { node } = path; - - if (!node.alternate) { - return; - } - - if (!t.isIfStatement(node.consequent)) { - return; - } - - if (t.isIfStatement(node.alternate)) { - return; - } - - node.test = t.unaryExpression("!", node.test, true); - [node.alternate, node.consequent] = [ - node.consequent, - node.alternate - ]; - }, - - // Make if statements with conditional returns in the body into - // an if statement that guards the rest of the block. - function(path) { - const { node } = path; - - 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)); - }, - + ifStatement.mergeNestedIfs, + ifStatement.simplify, + ifStatement.switchConsequent, + ifStatement.conditionalReturnToGuards, createPrevExpressionEater("if") ] }, @@ -1740,24 +1146,6 @@ module.exports = ({ types: t }) => { }; } - function isPatternMatchesPath(patternValue, inputPath) { - if (Array.isArray(patternValue)) { - for (let i = 0; i < patternValue.length; i++) { - if (isPatternMatchesPath(patternValue[i], inputPath)) { - return true; - } - } - return false; - } - if (typeof patternValue === "function") { - return patternValue(inputPath); - } - if (isNodeOfType(inputPath.node, patternValue)) return true; - const evalResult = inputPath.evaluate(); - if (!evalResult.confident || !inputPath.isPure()) return false; - return evalResult.value === patternValue; - } - // path1 -> path2 // is path1 an ancestor of path2 function isAncestor(path1, path2) { diff --git a/packages/babel-plugin-minify-simplify/src/logical-expression.js b/packages/babel-plugin-minify-simplify/src/logical-expression.js new file mode 100644 index 000000000..448d08e87 --- /dev/null +++ b/packages/babel-plugin-minify-simplify/src/logical-expression.js @@ -0,0 +1,79 @@ +"use strict"; + +const h = require("./helpers"); +const PatternMatch = require("./pattern-match"); + +module.exports = t => { + const OP_AND = input => input === "&&"; + const OP_OR = input => input === "||"; + + function simplifyPatterns(path) { + // cache of path.evaluate() + const evaluateMemo = new Map(); + + const TRUTHY = input => { + // !NaN and !undefined are truthy + // separate check here as they are considered impure by babel + if (input.isUnaryExpression() && input.get("argument").isIdentifier()) { + if ( + input.node.argument.name === "NaN" || + input.node.argument.name === "undefined" + ) { + return true; + } + } + const evalResult = input.evaluate(); + evaluateMemo.set(input, evalResult); + return evalResult.confident && input.isPure() && evalResult.value; + }; + + const FALSY = input => { + // NaN and undefined are falsy + // separate check here as they are considered impure by babel + if (input.isIdentifier()) { + if (input.node.name === "NaN" || input.node.name === "undefined") { + return true; + } + } + const evalResult = input.evaluate(); + evaluateMemo.set(input, evalResult); + return evalResult.confident && input.isPure() && !evalResult.value; + }; + + const { Expression: EX } = h.typeSymbols(t); + + // Convention: + // [left, operator, right, handler(leftNode, rightNode)] + const matcher = new PatternMatch([ + [TRUTHY, OP_AND, EX, (l, r) => r], + [FALSY, OP_AND, EX, l => l], + [TRUTHY, OP_OR, EX, l => l], + [FALSY, OP_OR, EX, (l, r) => r] + ]); + + const left = path.get("left"); + const right = path.get("right"); + const operator = path.node.operator; + + const result = matcher.match( + [left, operator, right], + h.isPatternMatchesPath(t) + ); + + if (result.match) { + // here we are sure that left.evaluate is always confident becuase + // it satisfied one of TRUTHY/FALSY paths + let value; + if (evaluateMemo.has(left)) { + value = evaluateMemo.get(left).value; + } else { + value = left.evaluate().value; + } + path.replaceWith(result.value(t.valueToNode(value), right.node)); + } + } + + return { + simplifyPatterns + }; +};