Skip to content

Commit

Permalink
Shorten assignments to itself (#230)
Browse files Browse the repository at this point in the history
* Simplify assignments to same identifier

* Add tests

* Bump versions

* Remove versioning

* Remove version

* Add support for optimizing member expressions

* Fix test name

* Remove comment

* Simplify ternary

* Fix bugs, simplify check

* Change postfix to prefix

* Exclude operators for member expressions too

* includes -> indexOf

* Fix lint

* Improve method name
  • Loading branch information
kangax committed Nov 13, 2016
1 parent c0e78cc commit 319d69d
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 0 deletions.
132 changes: 132 additions & 0 deletions packages/babel-plugin-minify-simplify/__tests__/simplify-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
107 changes: 107 additions & 0 deletions packages/babel-plugin-minify-simplify/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 === "||";

Expand Down Expand Up @@ -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'
Expand Down

0 comments on commit 319d69d

Please sign in to comment.