diff --git a/package.json b/package.json index 2d1bb11ff..8fa6a0fe1 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ }, "devDependencies": { "babel-core": "^6.18.0", - "babel-jest": "^14.1.0", + "babel-jest": "^16.0.0", "babel-plugin-transform-es2015-block-scoping": "^6.18.0", "babel-preset-es2015": "^6.18.0", "babel-traverse": "^6.16.0", @@ -43,7 +43,7 @@ "gulp-babel": "^6.1.2", "gulp-newer": "^1.1.0", "gulp-util": "^3.0.7", - "jest-cli": "^14.1.0", + "jest-cli": "^16.0.2", "lerna": "2.0.0-beta.26", "lerna-changelog": "^0.2.1", "through2": "^2.0.1", diff --git a/packages/babel-plugin-minify-simplify/__tests__/simplify-test.js b/packages/babel-plugin-minify-simplify/__tests__/simplify-test.js index d9f607848..5d36866d2 100644 --- a/packages/babel-plugin-minify-simplify/__tests__/simplify-test.js +++ b/packages/babel-plugin-minify-simplify/__tests__/simplify-test.js @@ -2107,6 +2107,163 @@ describe("simplify-plugin", () => { expect(transform(source)).toBe(expected); }); + // From UglifyJS + it("should simplify logical expression of the following forms of &&", () => { + // compress to right + let sources = unpad(` + a = true && foo + a = 1 && console.log("asdf") + a = 4 * 2 && foo() + a = 10 == 10 && foo() + bar() + a = "foo" && foo() + a = 1 + "a" && foo / 10 + a = -1 && 5 << foo + a = 6 && 10 + a = !NaN && foo() + `).split("\n"); + + let expected = unpad(` + a = foo; + a = console.log("asdf"); + a = foo(); + a = foo() + bar(); + a = foo(); + a = foo / 10; + a = 5 << foo; + a = 10; + a = foo(); + `).split("\n"); + + expect(sources.map((s) => transform(s))).toEqual(expected); + + // compress to left + sources = unpad(` + a = false && bar + a = NaN && console.log("a") + a = 0 && bar() + a = undefined && foo(bar) + a = 3 * 3 - 9 && bar(foo) + a = 9 == 10 && foo() + a = !"string" && foo % bar + a = 0 && 7 + `).split("\n"); + + expected = unpad(` + a = false; + a = NaN; + a = 0; + a = undefined; + a = 0; + a = false; + a = false; + a = 0; + `).split("\n"); + + expect(sources.map((s) => transform(s))).toEqual(expected); + + // don't compress + sources = unpad(` + a = foo() && true; + a = console.log && 3 + 8; + a = foo + bar + 5 && "a"; + a = 4 << foo && -1.5; + a = bar() && false; + a = foo() && 0; + a = bar() && NaN; + a = foo() && null; + `).split("\n"); + + expect(sources.map((s) => transform(s))).toEqual(sources); + }); + + it("should simplify logical expression of the following forms of ||", () => { + // compress to left + let sources = unpad(` + a = true || condition; + a = 1 || console.log("a"); + a = 2 * 3 || 2 * condition; + a = 5 == 5 || condition + 3; + a = "string" || 4 - condition; + a = 5 + "" || condition / 5; + a = -4.5 || 6 << condition; + a = 6 || 7; + `).split("\n"); + + let expected = unpad(` + a = true; + a = 1; + a = 6; + a = true; + a = "string"; + a = "5"; + a = -4.5; + a = 6; + `).split("\n"); + + expect(sources.map((s) => transform(s))).toEqual(expected); + + sources = unpad(` + a = false || condition; + a = 0 || console.log("b"); + a = NaN || console.log("c"); + a = undefined || 2 * condition; + a = null || condition + 3; + a = 2 * 3 - 6 || 4 - condition; + a = 10 == 7 || condition / 5; + a = !"string" || 6 % condition; + a = null || 7; + `).split("\n"); + + expected = unpad(` + a = condition; + a = console.log("b"); + a = console.log("c"); + a = 2 * condition; + a = condition + 3; + a = 4 - condition; + a = condition / 5; + a = 6 % condition; + a = 7; + `).split("\n"); + + expect(sources.map((s) => transform(s))).toEqual(expected); + + // don't compress + sources = unpad(` + a = condition || true; + a = console.log("a") || 2; + a = 4 - condition || "string"; + a = 6 << condition || -4.5; + a = condition || false; + a = console.log("b") || NaN; + a = console.log("c") || 0; + a = 2 * condition || undefined; + a = condition + 3 || null; + `).split("\n"); + + expect(sources.map((s) => transform(s))).toEqual(sources); + }); + + it("should transform complex logical expressions", () => { + let sources = unpad(` + a = true && 1 && foo + a = 1 && 4 * 2 && console.log("asdf") + a = 4 * 2 && NaN && foo() + a = 10 == 11 || undefined && foo() + bar() && bar() + a = -1 && undefined || 5 << foo + `).split("\n"); + + let expected = unpad(` + a = foo; + a = console.log("asdf"); + a = NaN; + a = undefined; + a = 5 << foo; + `).split("\n"); + + expect(sources.map((s) => transform(s))).toEqual(expected); + }); + // https://github.com/babel/babili/issues/115 it("should transform impure conditional statements correctly - issue#115", () => { const source = unpad(` diff --git a/packages/babel-plugin-minify-simplify/src/index.js b/packages/babel-plugin-minify-simplify/src/index.js index a18d5d2de..c8ce16350 100644 --- a/packages/babel-plugin-minify-simplify/src/index.js +++ b/packages/babel-plugin-minify-simplify/src/index.js @@ -28,6 +28,9 @@ module.exports = ({ types: t }) => { const or = (a, b) => t.logicalExpression("||", a, b); const and = (a, b) => t.logicalExpression("&&", a, b); + const OP_AND = (input) => input === "&&"; + const OP_OR = (input) => input === "||"; + return { name: "minify-simplify", visitor: { @@ -124,6 +127,73 @@ 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)); + } + } + }, + ConditionalExpression: { enter: [ // !foo ? 'foo' : 'bar' -> foo ? 'bar' : 'foo' @@ -1378,6 +1448,9 @@ module.exports = ({ types: t }) => { } return false; } + if (typeof patternValue === "function") { + return patternValue(inputPath); + } if (isNodeOfType(inputPath.node, patternValue)) return true; let evalResult = inputPath.evaluate(); if (!evalResult.confident || !inputPath.isPure()) return false; diff --git a/packages/babel-plugin-minify-simplify/src/pattern-match.js b/packages/babel-plugin-minify-simplify/src/pattern-match.js index 60ca7c91a..2d2383005 100644 --- a/packages/babel-plugin-minify-simplify/src/pattern-match.js +++ b/packages/babel-plugin-minify-simplify/src/pattern-match.js @@ -46,6 +46,8 @@ module.exports = class PatternMatch { result.value = current; break; } + } else { + break; } } return result;