diff --git a/packages/babel-plugin-minify-simplify/__tests__/simplify-test.js b/packages/babel-plugin-minify-simplify/__tests__/simplify-test.js index f35cf89ec..b57ba8ddc 100644 --- a/packages/babel-plugin-minify-simplify/__tests__/simplify-test.js +++ b/packages/babel-plugin-minify-simplify/__tests__/simplify-test.js @@ -2352,4 +2352,136 @@ describe("simplify-plugin", () => { const expected = source; expect(transform(source)).toBe(expected); }); + + it("should simplify assignments", () => { + + const source = unpad(` + x = x + 1, + x = x - 1, + x = x * 1, + x = x % 1, + x = x << 1, + x = x >> 1, + x = x >>> 1, + x = x & 1, + x = x | 1, + x = x ^ 1, + x = x / 1, + x = x ** 1; + `); + const expected = unpad(` + ++x, + --x, + x *= 1, + x %= 1, + x <<= 1, + x >>= 1, + x >>>= 1, + x &= 1, + x |= 1, + x ^= 1, + x /= 1, + x **= 1; + `).replace(/\s+/g, " "); + + expect(transform(source)).toBe(expected); + }); + + it("should simplify assignments 2", () => { + + const source = unpad(` + foo = foo + bar, + foo = foo * function(){}, + foo += 123, + foo = 1 + foo, + x = x = x + 1, + foo = foo + bar + baz + `); + const expected = unpad(` + foo += bar, + foo *= function () {}, + foo += 123, + foo = 1 + foo, + x = ++x, + foo = foo + bar + baz; + `).replace(/\s+/g, " "); + + expect(transform(source)).toBe(expected); + }); + + it("should simplify assignments w. member expressions", () => { + + const source = unpad(` + foo.bar = foo.bar + 1, + foo.bar = foo.bar + 2, + foo["x"] = foo[x] + 2, + foo[x] = foo[x] + 2, + foo[x] = foo["x"] + 2, + foo["x"] = foo["x"] + 2, + foo[1] = foo["1"] + 2, + foo["bar"] = foo["bar"] + 2, + foo[bar()] = foo[bar()] + 2, + foo[""] = foo[""] + 2, + foo[2] = foo[2] + 2, + foo[{}] = foo[{}] + 1, + foo[function(){}] = foo[function(){}] + 1, + foo[false] = foo[false] + 1, + foo.bar.baz = foo.bar.baz + 321, + this.hello = this.hello + 1, + foo[null] = foo[null] + 1, + foo[undefined] = foo[undefined] + 1, + foo.bar = foo.bar || {}; + `); + // TODO: foo[void 0] = foo[void 0] + 1; + const expected = unpad(` + ++foo.bar, + foo.bar += 2, + foo["x"] = foo[x] + 2, + foo[x] += 2, + foo[x] = foo["x"] + 2, + foo["x"] += 2, + foo[1] += 2, + foo["bar"] += 2, + foo[bar()] = foo[bar()] + 2, + foo[""] += 2, + foo[2] += 2, + foo[{}] = foo[{}] + 1, + foo[function () {}] = foo[function () {}] + 1, + ++foo[false], + foo.bar.baz += 321, + ++this.hello, + ++foo[null], + ++foo[undefined], + foo.bar = foo.bar || {}; + `).replace(/\s+/g, " "); + + expect(transform(source)).toBe(expected); + }); + + it("should simplify assignments w. super", () => { + + const source = unpad(` + class Foo { + foo() { + super.foo = super.foo + 1; + } + }; + `); + const expected = unpad(` + class Foo { + foo() { + ++super.foo; + } + }; + `); + + expect(transform(source)).toBe(expected); + }); + + it("should not simplify assignments w. template literals", () => { + + const source = unpad("foo[`x`] = foo[`x`] + 1;"); + + expect(transform(source)).toBe(source); + }); }); diff --git a/packages/babel-plugin-minify-simplify/src/index.js b/packages/babel-plugin-minify-simplify/src/index.js index 52165cef6..c2f4c52c4 100644 --- a/packages/babel-plugin-minify-simplify/src/index.js +++ b/packages/babel-plugin-minify-simplify/src/index.js @@ -28,6 +28,62 @@ module.exports = ({ types: t }) => { 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 === "||"; @@ -194,6 +250,57 @@ module.exports = ({ types: t }) => { } }, + AssignmentExpression(path) { + + const rightExpr = path.get("right"); + const leftExpr = path.get("left"); + + 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); + }, + ConditionalExpression: { enter: [ // !foo ? 'foo' : 'bar' -> foo ? 'bar' : 'foo'