diff --git a/lib/compress.js b/lib/compress.js index c2f4f8db..ad7c205a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -157,7 +157,7 @@ merge(Compressor.prototype, { var last_count = 1 / 0; for (var pass = 0; pass < passes; pass++) { if (pass > 0 || this.option("reduce_vars")) - node.reset_opt_flags(this, true); + node.reset_opt_flags(this); node = node.transform(this); if (passes > 1) { var count = 0; @@ -289,8 +289,9 @@ merge(Compressor.prototype, { self.transform(tt); }); - AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan) { - var reduce_vars = rescan && compressor.option("reduce_vars"); + AST_Node.DEFMETHOD("reset_opt_flags", function(compressor) { + var reduce_vars = compressor.option("reduce_vars"); + var unused = compressor.option("unused"); // Stack of look-up tables to keep track of whether a `SymbolDef` has been // properly assigned before use: // - `push()` & `pop()` when visiting conditional branches @@ -315,16 +316,31 @@ merge(Compressor.prototype, { if (node instanceof AST_SymbolRef) { var d = node.definition(); d.references.push(node); - if (d.fixed === undefined || !safe_to_read(d) - || is_modified(node, 0, is_immutable(node))) { + if (d.fixed === undefined || !safe_to_read(d) || d.single_use == "m") { d.fixed = false; } else { - var parent = tw.parent(); - if (parent instanceof AST_Assign && parent.operator == "=" && node === parent.right - || parent instanceof AST_Call && node !== parent.expression - || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope - || parent instanceof AST_VarDef && node === parent.value) { - d.escaped = true; + var value = node.fixed_value(); + if (unused) { + d.single_use = value + && d.references.length == 1 + && loop_ids[d.id] === in_loop + && d.scope === node.scope + && value.is_constant_expression(); + } + if (is_modified(node, 0, is_immutable(value))) { + if (d.single_use) { + d.single_use = "m"; + } else { + d.fixed = false; + } + } else { + var parent = tw.parent(); + if (parent instanceof AST_Assign && parent.operator == "=" && node === parent.right + || parent instanceof AST_Call && node !== parent.expression + || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope + || parent instanceof AST_VarDef && node === parent.value) { + d.escaped = true; + } } } } @@ -419,8 +435,7 @@ merge(Compressor.prototype, { pop(); return true; } - if (node instanceof AST_Binary - && (node.operator == "&&" || node.operator == "||")) { + if (node instanceof AST_Binary && lazy_op(node.operator)) { node.left.walk(tw); push(); node.right.walk(tw); @@ -567,20 +582,8 @@ merge(Compressor.prototype, { def.single_use = undefined; } - function is_immutable(node) { - var value = node.fixed_value(); - if (!value) return false; - if (value.is_constant()) return true; - if (compressor.option("unused")) { - var d = node.definition(); - if (d.single_use === undefined) { - d.single_use = loop_ids[d.id] === in_loop - && d.scope === node.scope - && value.is_constant_expression(); - } - if (d.references.length == 1 && d.single_use) return true; - } - return value instanceof AST_Lambda; + function is_immutable(value) { + return value && (value.is_constant() || value instanceof AST_Lambda); } function is_modified(node, level, immutable) { @@ -818,6 +821,7 @@ merge(Compressor.prototype, { // Locate symbols which may execute code outside of scanning range var lvalues = get_lvalues(candidate); if (lhs instanceof AST_SymbolRef) lvalues[lhs.name] = false; + var one_off = lhs instanceof AST_Symbol && lhs.definition().references.length == 1; var side_effects = value_has_side_effects(candidate); var hit = candidate.name instanceof AST_SymbolFunarg; var abort = false, replaced = false; @@ -879,17 +883,18 @@ merge(Compressor.prototype, { var sym; if (node instanceof AST_Call || node instanceof AST_Exit - || node instanceof AST_PropAccess && node.has_side_effects(compressor) + || node instanceof AST_PropAccess + && (side_effects || node.expression.may_throw_on_access(compressor)) || node instanceof AST_SymbolRef && (lvalues[node.name] || side_effects && !references_in_scope(node.definition())) - || (sym = lhs_or_def(node)) && get_symbol(sym).name in lvalues - || parent instanceof AST_Binary - && (parent.operator == "&&" || parent.operator == "||") - || parent instanceof AST_Case - || parent instanceof AST_Conditional - || parent instanceof AST_For - || parent instanceof AST_If) { + || (sym = lhs_or_def(node)) + && (sym instanceof AST_PropAccess || sym.name in lvalues) + || (side_effects || !one_off) + && (parent instanceof AST_Binary && lazy_op(parent.operator) + || parent instanceof AST_Case + || parent instanceof AST_Conditional + || parent instanceof AST_If)) { if (!(node instanceof AST_Scope)) descend(node, tt); abort = true; return node; @@ -1002,27 +1007,14 @@ merge(Compressor.prototype, { } } - function get_symbol(node) { - while (node instanceof AST_PropAccess) node = node.expression; - return node; - } - function get_lvalues(expr) { var lvalues = Object.create(null); if (expr instanceof AST_Unary) return lvalues; - var scope; var tw = new TreeWalker(function(node, descend) { - if (node instanceof AST_Scope) { - var save_scope = scope; - descend(); - scope = save_scope; - return true; - } - if (node instanceof AST_SymbolRef || node instanceof AST_PropAccess) { - var sym = get_symbol(node); - if (sym instanceof AST_SymbolRef) { - lvalues[sym.name] = lvalues[sym.name] || is_lhs(node, tw.parent()); - } + var sym = node; + while (sym instanceof AST_PropAccess) sym = sym.expression; + if (sym instanceof AST_SymbolRef || sym instanceof AST_This) { + lvalues[sym.name] = lvalues[sym.name] || is_lhs(node, tw.parent()); } }); expr[expr instanceof AST_Assign ? "right" : "value"].walk(tw); @@ -1521,9 +1513,10 @@ merge(Compressor.prototype, { return member(this.operator, unary_bool); }); def(AST_Binary, function(){ - return member(this.operator, binary_bool) || - ( (this.operator == "&&" || this.operator == "||") && - this.left.is_boolean() && this.right.is_boolean() ); + return member(this.operator, binary_bool) + || lazy_op(this.operator) + && this.left.is_boolean() + && this.right.is_boolean(); }); def(AST_Conditional, function(){ return this.consequent.is_boolean() && this.alternative.is_boolean(); @@ -1595,6 +1588,7 @@ merge(Compressor.prototype, { node.DEFMETHOD("is_string", func); }); + var lazy_op = makePredicate("&& ||"); var unary_side_effects = makePredicate("delete ++ --"); function is_lhs(node, parent) { @@ -2861,14 +2855,12 @@ merge(Compressor.prototype, { def(AST_Binary, function(compressor, first_in_statement){ var right = this.right.drop_side_effect_free(compressor); if (!right) return this.left.drop_side_effect_free(compressor, first_in_statement); - switch (this.operator) { - case "&&": - case "||": + if (lazy_op(this.operator)) { if (right === this.right) return this; var node = this.clone(); node.right = right; return node; - default: + } else { var left = this.left.drop_side_effect_free(compressor, first_in_statement); if (!left) return this.right.drop_side_effect_free(compressor, first_in_statement); return make_sequence(this, [ left, right ]); @@ -3792,7 +3784,7 @@ merge(Compressor.prototype, { } if (cdr instanceof AST_Binary && !(cdr instanceof AST_Assign)) { if (cdr.left.is_constant()) { - if (cdr.operator == "||" || cdr.operator == "&&") { + if (lazy_op(cdr.operator)) { expressions[++i] = expressions[j]; break; } @@ -4292,8 +4284,7 @@ merge(Compressor.prototype, { // "x" + (y + "z")==> "x" + y + "z" if (self.right instanceof AST_Binary && self.right.operator == self.operator - && (self.operator == "&&" - || self.operator == "||" + && (lazy_op(self.operator) || (self.operator == "+" && (self.right.left.is_string(compressor) || (self.left.is_string(compressor) @@ -4345,6 +4336,7 @@ merge(Compressor.prototype, { d.fixed = fixed = make_node(AST_Function, fixed, fixed); } if (compressor.option("unused") + && fixed && d.references.length == 1 && (d.single_use || is_func_expr(fixed) && !(d.scope.uses_arguments && d.orig[0] instanceof AST_SymbolFunarg) diff --git a/package.json b/package.json index b9eb72ad..7693cc41 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "https://github.com/mishoo/UglifyJS2/tree/harmony", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "3.1.4", + "version": "3.1.5", "engines": { "node": ">=0.8.0" }, diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index f11579d6..f7b66117 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -2874,3 +2874,419 @@ prop_side_effects_2: { "2", ] } + +issue_2365: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + console.log(function(a) { + var b = a.f; + a.f++; + return b; + }({ f: 1 })); + console.log(function() { + var a = { f: 1 }, b = a.f; + a.f++; + return b; + }()); + console.log({ + f: 1, + g: function() { + var b = this.f; + this.f++; + return b; + } + }.g()); + } + expect: { + console.log(function(a) { + var b = a.f; + a.f++; + return b; + }({ f: 1 })); + console.log(function() { + var a = { f: 1 }, b = a.f; + a.f++; + return b; + }()); + console.log({ + f: 1, + g: function() { + var b = this.f; + this.f++; + return b; + } + }.g()); + } + expect_stdout: [ + "1", + "1", + "1", + ] +} + +issue_2364_1: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function inc(obj) { + return obj.count++; + } + function foo() { + var first = arguments[0]; + var result = inc(first); + return foo.amount = first.count, result; + } + var data = { + count: 0, + }; + var answer = foo(data); + console.log(foo.amount, answer); + } + expect: { + function inc(obj) { + return obj.count++; + } + function foo() { + var first = arguments[0]; + var result = inc(first); + return foo.amount = first.count, result; + } + var data = { + count: 0 + }; + var answer = foo(data); + console.log(foo.amount, answer); + } + expect_stdout: "1 0" +} + +issue_2364_2: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function callValidate() { + var validate = compilation.validate; + var result = validate.apply(null, arguments); + return callValidate.errors = validate.errors, result; + } + } + expect: { + function callValidate() { + var validate = compilation.validate; + var result = validate.apply(null, arguments); + return callValidate.errors = validate.errors, result; + } + } +} + +issue_2364_3: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function inc(obj) { + return obj.count++; + } + function foo(bar) { + var result = inc(bar); + return foo.amount = bar.count, result; + } + var data = { + count: 0, + }; + var answer = foo(data); + console.log(foo.amount, answer); + } + expect: { + function inc(obj) { + return obj.count++; + } + function foo(bar) { + var result = inc(bar); + return foo.amount = bar.count, result; + } + var data = { + count: 0, + }; + var answer = foo(data); + console.log(foo.amount, answer); + } + expect_stdout: "1 0" +} + +issue_2364_4: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function inc(obj) { + return obj.count++; + } + function foo(bar, baz) { + var result = inc(bar); + return foo.amount = baz.count, result; + } + var data = { + count: 0, + }; + var answer = foo(data, data); + console.log(foo.amount, answer); + } + expect: { + function inc(obj) { + return obj.count++; + } + function foo(bar, baz) { + var result = inc(bar); + return foo.amount = baz.count, result; + } + var data = { + count: 0, + }; + var answer = foo(data, data); + console.log(foo.amount, answer); + } + expect_stdout: "1 0" +} + +issue_2364_5: { + options = { + collapse_vars: true, + evaluate: true, + pure_getters: true, + properties: true, + reduce_vars: true, + unused: true, + } + input: { + function f0(o, a, h) { + var b = 3 - a; + var obj = o; + var seven = 7; + var prop = 'run'; + var t = obj[prop](b)[seven] = h; + return t; + } + } + expect: { + function f0(o, a, h) { + return o.run(3 - a)[7] = h; + } + } +} + +issue_2364_6: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function f(a, b) { + var c = a.p; + b.p = "FAIL"; + return c; + } + var o = { + p: "PASS" + } + console.log(f(o, o)); + } + expect: { + function f(a, b) { + var c = a.p; + b.p = "FAIL"; + return c; + } + var o = { + p: "PASS" + } + console.log(f(o, o)); + } + expect_stdout: "PASS" +} + +issue_2364_7: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function f(a, b) { + var c = a.p; + b.f(); + return c; + } + var o = { + p: "PASS", + f: function() { + this.p = "FAIL"; + } + } + console.log(f(o, o)); + } + expect: { + function f(a, b) { + var c = a.p; + b.f(); + return c; + } + var o = { + p: "PASS", + f: function() { + this.p = "FAIL"; + } + } + console.log(f(o, o)); + } + expect_stdout: "PASS" +} + +issue_2364_8: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function f(a, b, c) { + var d = a[b.f = function() { + return "PASS"; + }]; + return c.f(d); + } + var o = { + f: function() { + return "FAIL"; + } + }; + console.log(f({}, o, o)); + } + expect: { + function f(a, b, c) { + var d = a[b.f = function() { + return "PASS"; + }]; + return c.f(d); + } + var o = { + f: function() { + return "FAIL"; + } + }; + console.log(f({}, o, o)); + } + expect_stdout: "PASS" +} + +issue_2364_9: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function f(a, b) { + var d = a(); + return b.f(d); + } + var o = { + f: function() { + return "FAIL"; + } + }; + console.log(f(function() { + o.f = function() { + return "PASS"; + }; + }, o)); + } + expect: { + function f(a, b) { + var d = a(); + return b.f(d); + } + var o = { + f: function() { + return "FAIL"; + } + }; + console.log(f(function() { + o.f = function() { + return "PASS"; + }; + }, o)); + } + expect_stdout: "PASS" +} + +pure_getters_chain: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + function o(t, r) { + var a = t[1], s = t[2], o = t[3], i = t[5]; + return a <= 23 && s <= 59 && o <= 59 && (!r || i); + } + console.log(o([ , 23, 59, 59, , 42], 1)); + } + expect: { + function o(t, r) { + return t[1] <= 23 && t[2] <= 59 && t[3] <= 59 && (!r || t[5]); + } + console.log(o([ , 23, 59, 59, , 42], 1)); + } + expect_stdout: "42" +} + +conditional_1: { + options = { + collapse_vars: true, + } + input: { + function f(a, b) { + var c = ""; + var d = b ? ">" : "<"; + if (a) c += "="; + return c += d; + } + console.log(f(0, 0), f(0, 1), f(1, 0), f(1, 1)); + } + expect: { + function f(a, b) { + var c = ""; + if (a) c += "="; + return c += b ? ">" : "<"; + } + console.log(f(0, 0), f(0, 1), f(1, 0), f(1, 1)); + } + expect_stdout: "< > =< =>" +} + +conditional_2: { + options = { + collapse_vars: true, + } + input: { + function f(a, b) { + var c = a + 1, d = a + 2; + return b ? c : d; + } + console.log(f(3, 0), f(4, 1)); + } + expect: { + function f(a, b) { + return b ? a + 1 : a + 2; + } + console.log(f(3, 0), f(4, 1)); + } + expect_stdout: "5 5" +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 0c758c0b..d78f1174 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -3051,3 +3051,63 @@ array_forof_2: { expect_stdout: "3" node_version: ">=0.12" } + +const_expr_1: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + var o = { + a: 1, + b: 2 + }; + o.a++; + console.log(o.a, o.b); + } + expect: { + var o = { + a: 1, + b: 2 + }; + o.a++; + console.log(o.a, o.b); + } + expect_stdout: "2 2" +} + +const_expr_2: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + Object.prototype.c = function() { + this.a++; + }; + var o = { + a: 1, + b: 2 + }; + o.c(); + console.log(o.a, o.b); + } + expect: { + Object.prototype.c = function() { + this.a++; + }; + var o = { + a: 1, + b: 2 + }; + o.c(); + console.log(o.a, o.b); + } + expect_stdout: "2 2" +}