From b49e142a26094ccb0a6e9f597e7363ba02280eb4 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 00:54:41 +0800 Subject: [PATCH 01/53] disable do{...}while(false) optimisation (#1534) - fails to handle `break` in body fixes #1532 --- lib/compress.js | 2 +- test/compress/loops.js | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 99235987..01fdeeae 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2132,7 +2132,7 @@ merge(Compressor.prototype, { } } else { // self instanceof AST_Do - return self.body; + return self; } } if (self instanceof AST_While) { diff --git a/test/compress/loops.js b/test/compress/loops.js index ca05461c..e26dc79f 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -213,6 +213,30 @@ evaluate: { a(); for(;;) c(); - d(); + // rule disabled due to issue_1532 + do d(); while (false); + } +} + +issue_1532: { + options = { + evaluate: true, + loops: true, + } + input: { + function f(x, y) { + do { + if (x) break; + foo(); + } while (false); + } + } + expect: { + function f(x, y) { + do { + if (x) break; + foo(); + } while (false); + } } } From fe9227a41bd13a8e58be1e17636a96d09e0fd956 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 00:56:06 +0800 Subject: [PATCH 02/53] fix reference marking in for-in loops (#1535) fixes #1533 --- lib/compress.js | 6 +++++- test/compress/reduce_vars.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 01fdeeae..d4b10b5d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -244,7 +244,11 @@ merge(Compressor.prototype, { } if (node instanceof AST_ForIn) { if (node.init instanceof AST_SymbolRef) { - node.init.definition().fixed = false; + var d = node.init.definition(); + d.references.push(node.init); + d.fixed = false; + } else { + node.init.walk(tw); } node.object.walk(tw); push(); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index e38c317b..87b1fc2e 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -589,3 +589,39 @@ inner_var_for_in: { x(1, b, c, d); } } + +issue_1533_1: { + options = { + collapse_vars: true, + reduce_vars: true, + } + input: { + var id = ""; + for (id in {break: "me"}) + console.log(id); + } + expect: { + var id = ""; + for (id in {break: "me"}) + console.log(id); + } +} + +issue_1533_2: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + var id = ""; + for (var id in {break: "me"}) + console.log(id); + console.log(id); + } + expect: { + var id = ""; + for (var id in {break: "me"}) + console.log(id); + console.log(id); + } +} From 70d72ad8065421908ff9a8658539359789c4f460 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 02:39:57 +0800 Subject: [PATCH 03/53] properly cover all cases of for-in loop variables (#1536) --- lib/compress.js | 12 ++++++------ test/compress/reduce_vars.js | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index d4b10b5d..f67f1d2c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -243,13 +243,13 @@ merge(Compressor.prototype, { return true; } if (node instanceof AST_ForIn) { - if (node.init instanceof AST_SymbolRef) { - var d = node.init.definition(); - d.references.push(node.init); - d.fixed = false; - } else { - node.init.walk(tw); + var sym = node.init; + if (sym instanceof AST_Var) { + sym = sym.definitions[0].name; } + var d = sym.definition(); + d.references.push(sym); + d.fixed = false; node.object.walk(tw); push(); node.body.walk(tw); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 87b1fc2e..557631bd 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -563,7 +563,7 @@ inner_var_for: { } } -inner_var_for_in: { +inner_var_for_in_1: { options = { evaluate: true, reduce_vars: true, @@ -590,6 +590,21 @@ inner_var_for_in: { } } +inner_var_for_in_2: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + for (var long_name in {}) + console.log(long_name); + } + expect: { + for (var long_name in {}) + console.log(long_name); + } +} + issue_1533_1: { options = { collapse_vars: true, From 4d63d4f5b30d2b46f3b6ed4cfced277f4f8e428f Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 2 Mar 2017 14:51:15 -0500 Subject: [PATCH 04/53] collapse_vars should not replace constant in for-in init section (#1538) fixes #1537 --- lib/compress.js | 10 +++++++--- test/compress/collapse_vars.js | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f67f1d2c..deb55ade 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -480,8 +480,12 @@ merge(Compressor.prototype, { // Constant single use vars can be replaced in any scope. if (var_decl.value.is_constant()) { var ctt = new TreeTransformer(function(node) { - if (node === ref) - return replace_var(node, ctt.parent(), true); + if (node === ref) { + var parent = ctt.parent(); + if (!(parent instanceof AST_ForIn && parent.init === node)) { + return replace_var(node, parent, true); + } + } }); stat.transform(ctt); continue; @@ -570,7 +574,7 @@ merge(Compressor.prototype, { // Further optimize statement after substitution. stat.reset_opt_flags(compressor); - compressor.warn("Replacing " + (is_constant ? "constant" : "variable") + + compressor.warn("Collapsing " + (is_constant ? "constant" : "variable") + " " + var_name + " [{file}:{line},{col}]", node.start); CHANGED = true; return value; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 5f63488f..82d943ff 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1315,3 +1315,17 @@ collapse_vars_regexp: { })(); } } + +issue_1537: { + options = { + collapse_vars: true, + } + input: { + var k = ''; + for (k in {prop: 'val'}){} + } + expect: { + var k = ''; + for (k in {prop: 'val'}); + } +} From 17b81350d46e369588523b421d4528212f0f3207 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 04:45:20 +0800 Subject: [PATCH 05/53] fix chained assignment with `unused` (#1540) When #1450 optimises `a=b=42`, it stops after the first variable even if both are unused. fixes #1539 --- lib/compress.js | 17 ++++++++++------- test/compress/drop-unused.js | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index deb55ade..0cc9c518 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1801,14 +1801,17 @@ merge(Compressor.prototype, { } return node; } - if (drop_vars && assign_as_unused - && node instanceof AST_Assign - && node.operator == "=" - && node.left instanceof AST_SymbolRef) { - var def = node.left.definition(); - if (!(def.id in in_use_ids) && self.variables.get(def.name) === def) { - return node.right; + if (drop_vars && assign_as_unused) { + var n = node; + while (n instanceof AST_Assign + && n.operator == "=" + && n.left instanceof AST_SymbolRef) { + var def = n.left.definition(); + if (def.id in in_use_ids + || self.variables.get(def.name) !== def) break; + n = n.right; } + if (n !== node) return n; } if (node instanceof AST_For) { descend(node, this); diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index c1ca1b55..4b613181 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -679,3 +679,24 @@ const_assign: { } } } + +issue_1539: { + options = { + cascade: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + function f() { + var a, b; + a = b = 42; + return a; + } + } + expect: { + function f() { + return 42; + } + } +} From e5cb9275df257751b7322f34bca5e76cc519b974 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 05:14:21 +0800 Subject: [PATCH 06/53] v2.8.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33bfc236..f152a51e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.4", + "version": "2.8.5", "engines": { "node": ">=0.8.0" }, From b5e0e8c2038c7c0ea13771891eb84f6e6f7bcbc3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 07:12:24 +0800 Subject: [PATCH 07/53] facilitate fix for #1531 (#1542) --- lib/compress.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 0cc9c518..38ebbf40 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -181,6 +181,13 @@ merge(Compressor.prototype, { var reduce_vars = rescan && compressor.option("reduce_vars"); var safe_ids = []; push(); + var suppressor = new TreeWalker(function(node) { + if (node instanceof AST_Symbol) { + var d = node.definition(); + if (node instanceof AST_SymbolRef) d.references.push(node); + d.fixed = false; + } + }); var tw = new TreeWalker(function(node){ if (!(node instanceof AST_Directive || node instanceof AST_Constant)) { node._squeezed = false; @@ -243,13 +250,7 @@ merge(Compressor.prototype, { return true; } if (node instanceof AST_ForIn) { - var sym = node.init; - if (sym instanceof AST_Var) { - sym = sym.definitions[0].name; - } - var d = sym.definition(); - d.references.push(sym); - d.fixed = false; + node.init.walk(suppressor); node.object.walk(tw); push(); node.body.walk(tw); From 18059cc94fdc037e296a1cb1b08143d5e3aae570 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 18:04:32 +0800 Subject: [PATCH 08/53] compress numerical expressions (#1513) safe operations - `a === b` => `a == b` - `a + -b` => `a - b` - `-a + b` => `b - a` - `a+ +b` => `+b+a` associative operations (bit-wise operations are safe, otherwise `unsafe_math`) - `a + (b + c)` => `(a + b) + c` - `(n + 2) + 3` => `5 + n` - `(2 * n) * 3` => `6 * n` - `(a | 1) | (2 | d)` => `(3 | a) | b` fixes #412 --- README.md | 3 + lib/compress.js | 171 +++++++++++++++++++++++++++++++++++++-- test/compress/numbers.js | 136 +++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 79064d79..628bcdec 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). comparison are switching. Compression only works if both `comparisons` and `unsafe_comps` are both set to true. +- `unsafe_math` (default: false) -- optimize numerical expressions like + `2 * x * 3` into `6 * x`, which may give imprecise floating point results. + - `unsafe_proto` (default: false) -- optimize expressions like `Array.prototype.slice.call(a)` into `[].slice.call(a)` diff --git a/lib/compress.js b/lib/compress.js index 38ebbf40..ec1e7174 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -54,6 +54,7 @@ function Compressor(options, false_by_default) { drop_debugger : !false_by_default, unsafe : false, unsafe_comps : false, + unsafe_math : false, unsafe_proto : false, conditionals : !false_by_default, comparisons : !false_by_default, @@ -1043,6 +1044,34 @@ merge(Compressor.prototype, { node.DEFMETHOD("is_boolean", func); }); + // methods to determine if an expression has a numeric result type + (function (def){ + def(AST_Node, return_false); + def(AST_Number, return_true); + var unary = makePredicate("+ - ~ ++ --"); + def(AST_Unary, function(){ + return unary(this.operator); + }); + var binary = makePredicate("- * / % & | ^ << >> >>>"); + def(AST_Binary, function(compressor){ + return binary(this.operator) || this.operator == "+" + && this.left.is_number(compressor) + && this.right.is_number(compressor); + }); + var assign = makePredicate("-= *= /= %= &= |= ^= <<= >>= >>>="); + def(AST_Assign, function(compressor){ + return assign(this.operator) || this.right.is_number(compressor); + }); + def(AST_Seq, function(compressor){ + return this.cdr.is_number(compressor); + }); + def(AST_Conditional, function(compressor){ + return this.consequent.is_number(compressor) && this.alternative.is_number(compressor); + }); + })(function(node, func){ + node.DEFMETHOD("is_number", func); + }); + // methods to determine if an expression has a string result type (function (def){ def(AST_Node, return_false); @@ -2867,8 +2896,14 @@ merge(Compressor.prototype, { right: rhs[0] }).optimize(compressor); } - function reverse(op, force) { - if (force || !(self.left.has_side_effects(compressor) || self.right.has_side_effects(compressor))) { + function reversible() { + return self.left instanceof AST_Constant + || self.right instanceof AST_Constant + || !self.left.has_side_effects(compressor) + && !self.right.has_side_effects(compressor); + } + function reverse(op) { + if (reversible()) { if (op) self.operator = op; var tmp = self.left; self.left = self.right; @@ -2884,7 +2919,7 @@ merge(Compressor.prototype, { if (!(self.left instanceof AST_Binary && PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) { - reverse(null, true); + reverse(); } } if (/^[!=]==?$/.test(self.operator)) { @@ -2919,6 +2954,7 @@ merge(Compressor.prototype, { case "===": case "!==": if ((self.left.is_string(compressor) && self.right.is_string(compressor)) || + (self.left.is_number(compressor) && self.right.is_number(compressor)) || (self.left.is_boolean() && self.right.is_boolean())) { self.operator = self.operator.substr(0, 2); } @@ -3056,7 +3092,10 @@ merge(Compressor.prototype, { } break; } - if (self.operator == "+") { + var associative = true; + switch (self.operator) { + case "+": + // "foo" + ("bar" + x) => "foobar" + x if (self.left instanceof AST_Constant && self.right instanceof AST_Binary && self.right.operator == "+" @@ -3064,7 +3103,7 @@ merge(Compressor.prototype, { && self.right.is_string(compressor)) { self = make_node(AST_Binary, self, { operator: "+", - left: make_node(AST_String, null, { + left: make_node(AST_String, self.left, { value: "" + self.left.getValue() + self.right.left.getValue(), start: self.left.start, end: self.right.left.end @@ -3072,6 +3111,7 @@ merge(Compressor.prototype, { right: self.right.right }); } + // (x + "foo") + "bar" => x + "foobar" if (self.right instanceof AST_Constant && self.left instanceof AST_Binary && self.left.operator == "+" @@ -3080,13 +3120,14 @@ merge(Compressor.prototype, { self = make_node(AST_Binary, self, { operator: "+", left: self.left.left, - right: make_node(AST_String, null, { + right: make_node(AST_String, self.right, { value: "" + self.left.right.getValue() + self.right.getValue(), start: self.left.right.start, end: self.right.end }) }); } + // (x + "foo") + ("bar" + y) => (x + "foobar") + y if (self.left instanceof AST_Binary && self.left.operator == "+" && self.left.is_string(compressor) @@ -3100,7 +3141,7 @@ merge(Compressor.prototype, { left: make_node(AST_Binary, self.left, { operator: "+", left: self.left.left, - right: make_node(AST_String, null, { + right: make_node(AST_String, self.left.right, { value: "" + self.left.right.getValue() + self.right.left.getValue(), start: self.left.right.start, end: self.right.left.end @@ -3109,6 +3150,122 @@ merge(Compressor.prototype, { right: self.right.right }); } + // a + -b => a - b + if (self.right instanceof AST_UnaryPrefix + && self.right.operator == "-" + && self.left.is_number(compressor)) { + self = make_node(AST_Binary, self, { + operator: "-", + left: self.left, + right: self.right.expression + }); + } + // -a + b => b - a + if (self.left instanceof AST_UnaryPrefix + && self.left.operator == "-" + && reversible() + && self.right.is_number(compressor)) { + self = make_node(AST_Binary, self, { + operator: "-", + left: self.right, + right: self.left.expression + }); + } + case "*": + associative = compressor.option("unsafe_math"); + case "&": + case "|": + case "^": + // a + +b => +b + a + if (self.left.is_number(compressor) + && self.right.is_number(compressor) + && reversible() + && !(self.left instanceof AST_Binary + && self.left.operator != self.operator + && PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) { + var reversed = make_node(AST_Binary, self, { + operator: self.operator, + left: self.right, + right: self.left + }); + if (self.right instanceof AST_Constant + && !(self.left instanceof AST_Constant)) { + self = best_of(reversed, self); + } else { + self = best_of(self, reversed); + } + } + if (associative && self.is_number(compressor)) { + // a + (b + c) => (a + b) + c + if (self.right instanceof AST_Binary + && self.right.operator == self.operator) { + self = make_node(AST_Binary, self, { + operator: self.operator, + left: make_node(AST_Binary, self.left, { + operator: self.operator, + left: self.left, + right: self.right.left, + start: self.left.start, + end: self.right.left.end + }), + right: self.right.right + }); + } + // (n + 2) + 3 => 5 + n + // (2 * n) * 3 => 6 + n + if (self.right instanceof AST_Constant + && self.left instanceof AST_Binary + && self.left.operator == self.operator) { + if (self.left.left instanceof AST_Constant) { + self = make_node(AST_Binary, self, { + operator: self.operator, + left: make_node(AST_Binary, self.left, { + operator: self.operator, + left: self.left.left, + right: self.right, + start: self.left.left.start, + end: self.right.end + }), + right: self.left.right + }); + } else if (self.left.right instanceof AST_Constant) { + self = make_node(AST_Binary, self, { + operator: self.operator, + left: make_node(AST_Binary, self.left, { + operator: self.operator, + left: self.left.right, + right: self.right, + start: self.left.right.start, + end: self.right.end + }), + right: self.left.left + }); + } + } + // (a | 1) | (2 | d) => (3 | a) | b + if (self.left instanceof AST_Binary + && self.left.operator == self.operator + && self.left.right instanceof AST_Constant + && self.right instanceof AST_Binary + && self.right.operator == self.operator + && self.right.left instanceof AST_Constant) { + self = make_node(AST_Binary, self, { + operator: self.operator, + left: make_node(AST_Binary, self.left, { + operator: self.operator, + left: make_node(AST_Binary, self.left.left, { + operator: self.operator, + left: self.left.right, + right: self.right.left, + start: self.left.right.start, + end: self.right.left.end + }), + right: self.left.left + }), + right: self.right.right + }); + } + } } } // x && (y && z) ==> x && y && z diff --git a/test/compress/numbers.js b/test/compress/numbers.js index 8e32ad02..0b40bb9c 100644 --- a/test/compress/numbers.js +++ b/test/compress/numbers.js @@ -17,3 +17,139 @@ hex_numbers_in_parentheses_for_prototype_functions: { } expect_exact: "-2;(-2).toFixed(0);2;2..toFixed(0);.2;.2.toFixed(0);2e-8;2e-8.toFixed(0);0xde0b6b3a7640080;(0xde0b6b3a7640080).toFixed(0);" } + +comparisons: { + options = { + comparisons: true, + } + input: { + console.log( + ~x === 42, + x % n === 42 + ); + } + expect: { + console.log( + 42 == ~x, + x % n == 42 + ); + } +} + +evaluate_1: { + options = { + evaluate: true, + unsafe_math: false, + } + input: { + console.log( + x + 1 + 2, + x * 1 * 2, + +x + 1 + 2, + 1 + x + 2 + 3, + 1 | x | 2 | 3, + 1 + x-- + 2 + 3, + 1 + (x*y + 2) + 3, + 1 + (2 + x + 3), + 1 + (2 + ~x + 3), + -y + (2 + ~x + 3), + 1 & (2 & x & 3), + 1 + (2 + (x |= 0) + 3) + ); + } + expect: { + console.log( + x + 1 + 2, + 1 * x * 2, + +x + 1 + 2, + 1 + x + 2 + 3, + 3 | x, + 1 + x-- + 2 + 3, + x*y + 2 + 1 + 3, + 1 + (2 + x + 3), + 2 + ~x + 3 + 1, + -y + (2 + ~x + 3), + 0 & x, + 2 + (x |= 0) + 3 + 1 + ); + } +} + +evaluate_2: { + options = { + evaluate: true, + unsafe_math: true, + } + input: { + console.log( + x + 1 + 2, + x * 1 * 2, + +x + 1 + 2, + 1 + x + 2 + 3, + 1 | x | 2 | 3, + 1 + x-- + 2 + 3, + 1 + (x*y + 2) + 3, + 1 + (2 + x + 3), + 1 & (2 & x & 3), + 1 + (2 + (x |= 0) + 3) + ); + } + expect: { + console.log( + x + 1 + 2, + 2 * x, + 3 + +x, + 1 + x + 2 + 3, + 3 | x, + 6 + x--, + 6 + x*y, + 1 + (2 + x + 3), + 0 & x, + 6 + (x |= 0) + ); + } +} + +evaluate_3: { + options = { + evaluate: true, + unsafe: true, + unsafe_math: true, + } + input: { + console.log(1 + Number(x) + 2); + } + expect: { + console.log(3 + +x); + } +} + +evaluate_4: { + options = { + evaluate: true, + } + input: { + console.log( + 1+ +a, + +a+1, + 1+-a, + -a+1, + +a+ +b, + +a+-b, + -a+ +b, + -a+-b + ); + } + expect: { + console.log( + +a+1, + +a+1, + 1-a, + 1-a, + +a+ +b, + +a-b, + -a+ +b, + -a-b + ); + } +} From 07accd2fbb78ddbdb427774b3b5287a16fa95b5f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 18:13:07 +0800 Subject: [PATCH 09/53] process code with implicit return statement (#1522) Bookmarklet for instance implicitedly assumes a "completion value" without using `return`. The `expression` option now supports such use cases. Optimisations on IIFEs also enhanced. fixes #354 fixes #543 fixes #625 fixes #628 fixes #640 closes #1293 --- README.md | 3 + lib/compress.js | 90 +++++++--- test/compress/drop-unused.js | 2 +- test/compress/evaluate.js | 8 +- test/compress/functions.js | 8 +- test/compress/issue-640.js | 317 +++++++++++++++++++++++++++++++++++ test/compress/negate-iife.js | 87 ++++++++++ test/compress/sequences.js | 2 +- 8 files changed, 485 insertions(+), 32 deletions(-) create mode 100644 test/compress/issue-640.js diff --git a/README.md b/README.md index 628bcdec..0b532a83 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). such as `console.info` and/or retain side effects from function arguments after dropping the function call then use `pure_funcs` instead. +- `expression` -- default `false`. Pass `true` to preserve completion values + from terminal statements without `return`, e.g. in bookmarklets. + - `keep_fargs` -- default `true`. Prevents the compressor from discarding unused function arguments. You need this for code which relies on `Function.length`. diff --git a/lib/compress.js b/lib/compress.js index ec1e7174..2cd79128 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -80,6 +80,7 @@ function Compressor(options, false_by_default) { screw_ie8 : true, drop_console : false, angular : false, + expression : false, warnings : true, global_defs : {}, passes : 1, @@ -116,12 +117,18 @@ Compressor.prototype = new TreeTransformer; merge(Compressor.prototype, { option: function(key) { return this.options[key] }, compress: function(node) { + if (this.option("expression")) { + node = node.process_expression(true); + } var passes = +this.options.passes || 1; for (var pass = 0; pass < passes && pass < 3; ++pass) { if (pass > 0 || this.option("reduce_vars")) node.reset_opt_flags(this, true); node = node.transform(this); } + if (this.option("expression")) { + node = node.process_expression(false); + } return node; }, warn: function(text, props) { @@ -178,6 +185,42 @@ merge(Compressor.prototype, { return this.print_to_string() == node.print_to_string(); }); + AST_Node.DEFMETHOD("process_expression", function(insert) { + var self = this; + var tt = new TreeTransformer(function(node) { + if (insert && node instanceof AST_SimpleStatement) { + return make_node(AST_Return, node, { + value: node.body + }); + } + if (!insert && node instanceof AST_Return) { + return make_node(AST_SimpleStatement, node, { + body: node.value || make_node(AST_Undefined, node) + }); + } + if (node instanceof AST_Lambda && node !== self) { + return node; + } + if (node instanceof AST_Block) { + var index = node.body.length - 1; + if (index >= 0) { + node.body[index] = node.body[index].transform(tt); + } + } + if (node instanceof AST_If) { + node.body = node.body.transform(tt); + if (node.alternative) { + node.alternative = node.alternative.transform(tt); + } + } + if (node instanceof AST_With) { + node.body = node.body.transform(tt); + } + return node; + }); + return self.transform(tt); + }); + AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan){ var reduce_vars = rescan && compressor.option("reduce_vars"); var safe_ids = []; @@ -2030,7 +2073,14 @@ merge(Compressor.prototype, { def(AST_Constant, return_null); def(AST_This, return_null); def(AST_Call, function(compressor, first_in_statement){ - if (!this.has_pure_annotation(compressor) && compressor.pure_funcs(this)) return this; + if (!this.has_pure_annotation(compressor) && compressor.pure_funcs(this)) { + if (this.expression instanceof AST_Function) { + var node = this.clone(); + node.expression = node.expression.process_expression(false); + return node; + } + return this; + } if (this.pure) { compressor.warn("Dropping __PURE__ call [{file}:{line},{col}]", this.start); this.pure.value = this.pure.value.replace(/[@#]__PURE__/g, ' '); @@ -2522,12 +2572,13 @@ merge(Compressor.prototype, { }); OPT(AST_Call, function(self, compressor){ + var exp = self.expression; if (compressor.option("unused") - && self.expression instanceof AST_Function - && !self.expression.uses_arguments - && !self.expression.uses_eval - && self.args.length > self.expression.argnames.length) { - var end = self.expression.argnames.length; + && exp instanceof AST_Function + && !exp.uses_arguments + && !exp.uses_eval + && self.args.length > exp.argnames.length) { + var end = exp.argnames.length; for (var i = end, len = self.args.length; i < len; i++) { var node = self.args[i].drop_side_effect_free(compressor); if (node) { @@ -2537,7 +2588,6 @@ merge(Compressor.prototype, { self.args.length = end; } if (compressor.option("unsafe")) { - var exp = self.expression; if (exp instanceof AST_SymbolRef && exp.undeclared()) { switch (exp.name) { case "Array": @@ -2711,16 +2761,22 @@ merge(Compressor.prototype, { return best_of(self, node); } } - if (compressor.option("side_effects")) { - if (self.expression instanceof AST_Function - && self.args.length == 0 - && !AST_Block.prototype.has_side_effects.call(self.expression, compressor)) { - return make_node(AST_Undefined, self).transform(compressor); + if (exp instanceof AST_Function) { + if (exp.body[0] instanceof AST_Return + && exp.body[0].value.is_constant()) { + var args = self.args.concat(exp.body[0].value); + return AST_Seq.from_array(args).transform(compressor); + } + if (compressor.option("side_effects")) { + if (!AST_Block.prototype.has_side_effects.call(exp, compressor)) { + var args = self.args.concat(make_node(AST_Undefined, self)); + return AST_Seq.from_array(args).transform(compressor); + } } } if (compressor.option("drop_console")) { - if (self.expression instanceof AST_PropAccess) { - var name = self.expression.expression; + if (exp instanceof AST_PropAccess) { + var name = exp.expression; while (name.expression) { name = name.expression; } @@ -2731,12 +2787,6 @@ merge(Compressor.prototype, { } } } - if (self.args.length == 0 - && self.expression instanceof AST_Function - && self.expression.body[0] instanceof AST_Return - && self.expression.body[0].value.is_constant()) { - return self.expression.body[0].value; - } if (compressor.option("negate_iife") && compressor.parent() instanceof AST_SimpleStatement && is_iife_call(self)) { diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 4b613181..20dab3b9 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -632,7 +632,7 @@ iife: { } expect: { function f() { - ~function() {}(b); + b; } } } diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 26b6e489..68739503 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -640,9 +640,7 @@ call_args: { expect: { const a = 1; console.log(1); - +function(a) { - return 1; - }(1); + +(1, 1); } } @@ -663,9 +661,7 @@ call_args_drop_param: { expect: { const a = 1; console.log(1); - +function() { - return 1; - }(b); + +(b, 1); } } diff --git a/test/compress/functions.js b/test/compress/functions.js index d3d99b57..18505a18 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -35,9 +35,9 @@ iifes_returning_constants_keep_fargs_true: { console.log("okay"); console.log(123); console.log(void 0); - console.log(function(x,y,z){return 2}(1,2,3)); - console.log(function(x,y){return 6}(2,3)); - console.log(function(x, y){return 6}(2,3,a(),b())); + console.log(2); + console.log(6); + console.log((a(), b(), 6)); } } @@ -71,6 +71,6 @@ iifes_returning_constants_keep_fargs_false: { console.log(void 0); console.log(2); console.log(6); - console.log(function(){return 6}(a(),b())); + console.log((a(), b(), 6)); } } diff --git a/test/compress/issue-640.js b/test/compress/issue-640.js new file mode 100644 index 00000000..dd3f3f21 --- /dev/null +++ b/test/compress/issue-640.js @@ -0,0 +1,317 @@ +cond_5: { + options = { + conditionals: true, + expression: true, + } + input: { + if (some_condition()) { + if (some_other_condition()) { + do_something(); + } else { + alternate(); + } + } else { + alternate(); + } + + if (some_condition()) { + if (some_other_condition()) { + do_something(); + } + } + } + expect: { + some_condition() && some_other_condition() ? do_something() : alternate(); + if (some_condition() && some_other_condition()) do_something(); + } +} + +dead_code_const_annotation_regex: { + options = { + booleans : true, + conditionals : true, + dead_code : true, + evaluate : true, + expression : true, + loops : true, + } + input: { + var unused; + // @constraint this shouldn't be a constant + var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("reachable"); + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + if (CONST_FOO_ANN) console.log('reachable'); + } +} + +drop_console_2: { + options = { + drop_console: true, + expression: true, + } + input: { + console.log('foo'); + console.log.apply(console, arguments); + } + expect: { + // with regular compression these will be stripped out as well + void 0; + void 0; + } +} + +drop_value: { + options = { + expression: true, + side_effects: true, + } + input: { + (1, [2, foo()], 3, {a:1, b:bar()}); + } + expect: { + foo(), {a:1, b:bar()}; + } +} + +wrongly_optimized: { + options = { + conditionals: true, + booleans: true, + evaluate: true, + expression: true, + } + input: { + function func() { + foo(); + } + if (func() || true) { + bar(); + } + } + expect: { + function func() { + foo(); + } + // TODO: optimize to `func(), bar()` + if (func(), !0) bar(); + } +} + +negate_iife_1: { + options = { + expression: true, + negate_iife: true, + } + input: { + (function(){ stuff() })(); + } + expect: { + (function(){ stuff() })(); + } +} + +negate_iife_3: { + options = { + conditionals: true, + expression: true, + negate_iife: true, + } + input: { + (function(){ return t })() ? console.log(true) : console.log(false); + } + expect: { + (function(){ return t })() ? console.log(true) : console.log(false); + } +} + +negate_iife_3_off: { + options = { + conditionals: true, + expression: true, + negate_iife: false, + } + input: { + (function(){ return t })() ? console.log(true) : console.log(false); + } + expect: { + (function(){ return t })() ? console.log(true) : console.log(false); + } +} + +negate_iife_4: { + options = { + conditionals: true, + expression: true, + negate_iife: true, + sequences: true, + } + input: { + (function(){ return t })() ? console.log(true) : console.log(false); + (function(){ + console.log("something"); + })(); + } + expect: { + (function(){ return t })() ? console.log(true) : console.log(false), function(){ + console.log("something"); + }(); + } +} + +negate_iife_5: { + options = { + conditionals: true, + expression: true, + negate_iife: true, + sequences: true, + } + input: { + if ((function(){ return t })()) { + foo(true); + } else { + bar(false); + } + (function(){ + console.log("something"); + })(); + } + expect: { + (function(){ return t })() ? foo(true) : bar(false), function(){ + console.log("something"); + }(); + } +} + +negate_iife_5_off: { + options = { + conditionals: true, + expression: true, + negate_iife: false, + sequences: true, + }; + input: { + if ((function(){ return t })()) { + foo(true); + } else { + bar(false); + } + (function(){ + console.log("something"); + })(); + } + expect: { + (function(){ return t })() ? foo(true) : bar(false), function(){ + console.log("something"); + }(); + } +} + +issue_1254_negate_iife_true: { + options = { + expression: true, + negate_iife: true, + } + input: { + (function() { + return function() { + console.log('test') + }; + })()(); + } + expect_exact: '(function(){return function(){console.log("test")}})()();' +} + +issue_1254_negate_iife_nested: { + options = { + expression: true, + negate_iife: true, + } + input: { + (function() { + return function() { + console.log('test') + }; + })()()()()(); + } + expect_exact: '(function(){return function(){console.log("test")}})()()()()();' +} + +conditional: { + options = { + expression: true, + pure_funcs: [ "pure" ], + side_effects: true, + } + input: { + pure(1 | a() ? 2 & b() : 7 ^ c()); + pure(1 | a() ? 2 & b() : 5); + pure(1 | a() ? 4 : 7 ^ c()); + pure(1 | a() ? 4 : 5); + pure(3 ? 2 & b() : 7 ^ c()); + pure(3 ? 2 & b() : 5); + pure(3 ? 4 : 7 ^ c()); + pure(3 ? 4 : 5); + } + expect: { + 1 | a() ? b() : c(); + 1 | a() && b(); + 1 | a() || c(); + a(); + 3 ? b() : c(); + 3 && b(); + 3 || c(); + pure(3 ? 4 : 5); + } +} + +limit_1: { + options = { + expression: true, + sequences: 3, + } + input: { + a; + b; + c; + d; + e; + f; + g; + h; + i; + j; + k; + } + expect: { + // Turned into a single return statement + // so it can no longer be split into lines + a,b,c,d,e,f,g,h,i,j,k; + } +} + +iife: { + options = { + expression: true, + sequences: true, + } + input: { + x = 42; + (function a() {})(); + !function b() {}(); + ~function c() {}(); + +function d() {}(); + -function e() {}(); + void function f() {}(); + typeof function g() {}(); + } + expect: { + x = 42, function a() {}(), function b() {}(), function c() {}(), + function d() {}(), function e() {}(), function f() {}(), typeof function g() {}(); + } +} diff --git a/test/compress/negate-iife.js b/test/compress/negate-iife.js index f17ae206..9a0b5a46 100644 --- a/test/compress/negate-iife.js +++ b/test/compress/negate-iife.js @@ -32,6 +32,19 @@ negate_iife_2: { } } +negate_iife_2_side_effects: { + options = { + negate_iife: true, + side_effects: true, + } + input: { + (function(){ return {} })().x = 10; // should not transform this one + } + expect: { + (function(){ return {} })().x = 10; + } +} + negate_iife_3: { options = { negate_iife: true, @@ -45,6 +58,34 @@ negate_iife_3: { } } +negate_iife_3_evaluate: { + options = { + conditionals: true, + evaluate: true, + negate_iife: true, + } + input: { + (function(){ return true })() ? console.log(true) : console.log(false); + } + expect: { + console.log(true); + } +} + +negate_iife_3_side_effects: { + options = { + conditionals: true, + negate_iife: true, + side_effects: true, + } + input: { + (function(){ return t })() ? console.log(true) : console.log(false); + } + expect: { + !function(){ return t }() ? console.log(false) : console.log(true); + } +} + negate_iife_3_off: { options = { negate_iife: false, @@ -58,6 +99,20 @@ negate_iife_3_off: { } } +negate_iife_3_off_evaluate: { + options = { + conditionals: true, + evaluate: true, + negate_iife: false, + } + input: { + (function(){ return true })() ? console.log(true) : console.log(false); + } + expect: { + console.log(true); + } +} + negate_iife_4: { options = { negate_iife: true, @@ -320,3 +375,35 @@ issue_1288: { }(0); } } + +issue_1288_side_effects: { + options = { + conditionals: true, + negate_iife: true, + side_effects: true, + } + input: { + if (w) ; + else { + (function f() {})(); + } + if (!x) { + (function() { + x = {}; + })(); + } + if (y) + (function() {})(); + else + (function(z) { + return z; + })(0); + } + expect: { + w; + x || function() { + x = {}; + }(); + y; + } +} diff --git a/test/compress/sequences.js b/test/compress/sequences.js index d93f5237..7bb274cb 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -248,6 +248,6 @@ iife: { } expect: { x = 42, function a() {}(), function b() {}(), function c() {}(), - function d() {}(), function e() {}(), function f() {}(), function g() {}() + function d() {}(), function e() {}(), function f() {}(), function g() {}(); } } From ce54c9cceef68b78be7cc429988df26add904d9b Mon Sep 17 00:00:00 2001 From: kzc Date: Fri, 3 Mar 2017 13:39:54 -0500 Subject: [PATCH 10/53] disallow collapse_vars constant replacement in for-in statements (#1543) --- lib/compress.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 2cd79128..35459006 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -525,11 +525,9 @@ merge(Compressor.prototype, { // Constant single use vars can be replaced in any scope. if (var_decl.value.is_constant()) { var ctt = new TreeTransformer(function(node) { - if (node === ref) { - var parent = ctt.parent(); - if (!(parent instanceof AST_ForIn && parent.init === node)) { - return replace_var(node, parent, true); - } + if (node === ref + && !ctt.find_parent(AST_ForIn)) { + return replace_var(node, ctt.parent(), true); } }); stat.transform(ctt); From ea9ab9fb0e48b4bf011da67f86652f148ae6ed1a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 01:54:20 +0800 Subject: [PATCH 11/53] resolve issue with outdated version of async (#1549) fixes #746 --- bin/uglifyjs | 12 ++++++++---- package.json | 1 - 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/bin/uglifyjs b/bin/uglifyjs index 367d66e2..4dd234dc 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -8,7 +8,6 @@ var sys = require("util"); var yargs = require("yargs"); var fs = require("fs"); var path = require("path"); -var async = require("async"); var acorn; var screw_ie8 = true; var ARGS = yargs @@ -319,8 +318,11 @@ var STATS = {}; var TOPLEVEL = null; var P_RELATIVE = ARGS.p && ARGS.p == "relative"; var SOURCES_CONTENT = {}; +var index = 0; -async.eachLimit(files, 1, function (file, cb) { +!function cb() { + if (index == files.length) return done(); + var file = files[index++]; read_whole_file(file, function (err, code) { if (err) { print_error("ERROR: can't read file: " + file); @@ -388,7 +390,9 @@ async.eachLimit(files, 1, function (file, cb) { }); cb(); }); -}, function () { +}(); + +function done() { var OUTPUT_FILE = ARGS.o; var SOURCE_MAP = (ARGS.source_map || ARGS.source_map_inline) ? UglifyJS.SourceMap({ @@ -537,7 +541,7 @@ async.eachLimit(files, 1, function (file, cb) { })); } } -}); +} /* -----[ functions ]----- */ diff --git a/package.json b/package.json index f152a51e..4512eb71 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "LICENSE" ], "dependencies": { - "async": "~0.2.6", "source-map": "~0.5.1", "uglify-to-browserify": "~1.0.0", "yargs": "~3.10.0" From 78d1bb92d4560b73099afddd3bd2a85641bf3002 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 12:12:59 +0800 Subject: [PATCH 12/53] fix a corner case in #1530 (#1552) --- lib/compress.js | 10 ++++++---- test/compress/functions.js | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 35459006..7eead2c1 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2760,10 +2760,12 @@ merge(Compressor.prototype, { } } if (exp instanceof AST_Function) { - if (exp.body[0] instanceof AST_Return - && exp.body[0].value.is_constant()) { - var args = self.args.concat(exp.body[0].value); - return AST_Seq.from_array(args).transform(compressor); + if (exp.body[0] instanceof AST_Return) { + var value = exp.body[0].value; + if (!value || value.is_constant()) { + var args = self.args.concat(value || make_node(AST_Undefined, self)); + return AST_Seq.from_array(args).transform(compressor); + } } if (compressor.option("side_effects")) { if (!AST_Block.prototype.has_side_effects.call(exp, compressor)) { diff --git a/test/compress/functions.js b/test/compress/functions.js index 18505a18..a1a515a1 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -74,3 +74,20 @@ iifes_returning_constants_keep_fargs_false: { console.log((a(), b(), 6)); } } + +issue_485_crashing_1530: { + options = { + conditionals: true, + dead_code: true, + evaluate: true, + } + input: { + (function(a) { + if (true) return; + var b = 42; + })(this); + } + expect: { + this, void 0; + } +} From eb98a7f2f38f5de16b50560199ee7ec719a1e945 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 12:16:02 +0800 Subject: [PATCH 13/53] fix handling of shebang and preamble (#1545) fixes #1332 --- lib/output.js | 38 +++++++++++++++--------------------- lib/parse.js | 12 +++++------- test/mocha/comment-filter.js | 8 ++++++++ 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/output.js b/lib/output.js index 4a0a1e0e..10fed135 100644 --- a/lib/output.js +++ b/lib/output.js @@ -46,17 +46,8 @@ var EXPECT_DIRECTIVE = /^$|[;{][\s\n]*$/; function is_some_comments(comment) { - var text = comment.value; - var type = comment.type; - if (type == "comment2") { - // multiline comment - return /@preserve|@license|@cc_on/i.test(text); - } - return type == "comment5"; -} - -function is_comment5(comment) { - return comment.type == "comment5"; + // multiline comment + return comment.type == "comment2" && /@preserve|@license|@cc_on/i.test(comment.value); } function OutputStream(options) { @@ -86,7 +77,7 @@ function OutputStream(options) { }, true); // Convert comment option to RegExp if neccessary and set up comments filter - var comment_filter = options.shebang ? is_comment5 : return_false; // Default case, throw all comments away except shebangs + var comment_filter = return_false; // Default case, throw all comments away if (options.comments) { var comments = options.comments; if (typeof options.comments === "string" && /^\/.*\/[a-zA-Z]*$/.test(options.comments)) { @@ -98,12 +89,12 @@ function OutputStream(options) { } if (comments instanceof RegExp) { comment_filter = function(comment) { - return comment.type == "comment5" || comments.test(comment.value); + return comment.type != "comment5" && comments.test(comment.value); }; } else if (typeof comments === "function") { comment_filter = function(comment) { - return comment.type == "comment5" || comments(this, comment); + return comment.type != "comment5" && comments(this, comment); }; } else if (comments === "some") { @@ -400,10 +391,6 @@ function OutputStream(options) { return OUTPUT; }; - if (options.preamble) { - print(options.preamble.replace(/\r\n?|[\n\u2028\u2029]|\s*$/g, "\n")); - } - var stack = []; return { get : get, @@ -523,6 +510,17 @@ function OutputStream(options) { })); } + if (comments.length > 0 && output.pos() == 0) { + if (output.option("shebang") && comments[0].type == "comment5") { + output.print("#!" + comments.shift().value + "\n"); + output.indent(); + } + var preamble = output.option("preamble"); + if (preamble) { + output.print(preamble.replace(/\r\n?|[\n\u2028\u2029]|\s*$/g, "\n")); + } + } + comments = comments.filter(output.comment_filter, self); // Keep single line comments after nlb, after nlb @@ -547,10 +545,6 @@ function OutputStream(options) { output.space(); } } - else if (output.pos() === 0 && c.type == "comment5" && output.option("shebang")) { - output.print("#!" + c.value + "\n"); - output.indent(); - } }); } }); diff --git a/lib/parse.js b/lib/parse.js index 9b198ccd..9aadc9f5 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -558,6 +558,11 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { function next_token(force_regexp) { if (force_regexp != null) return read_regexp(force_regexp); + if (shebang && S.pos == 0 && looking_at("#!")) { + start_token(); + forward(2); + skip_line_comment("comment5"); + } for (;;) { skip_whitespace(); start_token(); @@ -589,13 +594,6 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { if (PUNC_CHARS(ch)) return token("punc", next()); if (OPERATOR_CHARS(ch)) return read_operator(); if (code == 92 || is_identifier_start(code)) return read_word(); - if (shebang) { - if (S.pos == 0 && looking_at("#!")) { - forward(2); - skip_line_comment("comment5"); - continue; - } - } break; } parse_error("Unexpected character '" + ch + "'"); diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js index 01580c87..9474e732 100644 --- a/test/mocha/comment-filter.js +++ b/test/mocha/comment-filter.js @@ -72,4 +72,12 @@ describe("comment filters", function() { assert.strictEqual(UglifyJS.parse("/* ok */ function a(){}").print_to_string(options), "/* ok */function a(){}"); assert.strictEqual(UglifyJS.parse("/* ok */ function a(){}").print_to_string(options), "/* ok */function a(){}"); }); + + it("Should handle shebang and preamble correctly", function() { + var code = UglifyJS.minify("#!/usr/bin/node\nvar x = 10;", { + fromString: true, + output: { preamble: "/* Build */" } + }).code; + assert.strictEqual(code, "#!/usr/bin/node\n/* Build */\nvar x=10;"); + }) }); From 1f0333e9f146311e0e412fbd0783c0e1e63c7802 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 12:51:11 +0800 Subject: [PATCH 14/53] stay safe with constants in IE8- (#1547) - `undefined` etc. can be redefined at top-level for IE8-, so disable related optimisations - fixed `--support-ie8` catch mangle bug --- README.md | 2 -- bin/uglifyjs | 2 +- lib/compress.js | 4 +++- lib/scope.js | 22 +++++++++++++++++--- test/compress/screw-ie8.js | 42 ++++++++++++++++++++++++++------------ 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 0b532a83..f880fd7b 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,6 @@ The available options are: --support-ie8 Use this flag to support Internet Explorer 6/7/8. Equivalent to setting `screw_ie8: false` in `minify()` for `compress`, `mangle` and `output` options. - Note: `--support-ie8` may generate incorrect code - for `try`/`catch` in ES5 compliant browsers. --expr Parse a single expression, rather than a program (for parsing JSON) -p, --prefix Skip prefix for original filenames that appear diff --git a/bin/uglifyjs b/bin/uglifyjs index 4dd234dc..e39a4b4b 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -26,7 +26,7 @@ mangling you need to use `-c` and `-m`.\ .describe("source-map-include-sources", "Pass this flag if you want to include the content of source files in the source map as sourcesContent property.") .describe("in-source-map", "Input source map, useful if you're compressing JS that was generated from some other original code.") .describe("screw-ie8", "Do not support Internet Explorer 6/7/8. This flag is enabled by default.") - .describe("support-ie8", "Support non-standard Internet Explorer 6/7/8 javascript. Note: may generate incorrect code for try/catch in ES5 compliant browsers.") + .describe("support-ie8", "Support non-standard Internet Explorer 6/7/8 javascript.") .describe("expr", "Parse a single expression, rather than a program (for parsing JSON)") .describe("p", "Skip prefix for original filenames that appear in source maps. \ For example -p 3 will drop 3 directories from file names and ensure they are relative paths. \ diff --git a/lib/compress.js b/lib/compress.js index 7eead2c1..4a5f6395 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3348,7 +3348,9 @@ merge(Compressor.prototype, { return def; } // testing against !self.scope.uses_with first is an optimization - if (self.undeclared() && !isLHS(self, compressor.parent()) + if (compressor.option("screw_ie8") + && self.undeclared() + && !isLHS(self, compressor.parent()) && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { switch (self.name) { case "undefined": diff --git a/lib/scope.js b/lib/scope.js index ae0c5777..b00fcb4a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -97,7 +97,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var labels = new Dictionary(); var defun = null; var tw = new TreeWalker(function(node, descend){ - if (options.screw_ie8 && node instanceof AST_Catch) { + if (node instanceof AST_Catch) { var save_scope = scope; scope = new AST_Scope(node); scope.init_scope_vars(); @@ -158,8 +158,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { - (options.screw_ie8 ? scope : defun) - .def_variable(node); + scope.def_variable(node); } else if (node instanceof AST_LabelRef) { var sym = labels.get(node.name); @@ -209,6 +208,23 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ }); self.walk(tw); + // pass 3: fix up any scoping issue with IE8 + if (!options.screw_ie8) { + self.walk(new TreeWalker(function(node, descend) { + if (node instanceof AST_SymbolCatch) { + var name = node.name; + var scope = node.thedef.scope.parent_scope; + var def = scope.find_variable(name) || self.globals.get(name) || scope.def_variable(node); + node.thedef.references.forEach(function(ref) { + ref.thedef = def; + ref.reference(options); + }); + node.thedef = def; + return true; + } + })); + } + if (options.cache) { this.cname = options.cache.cname; } diff --git a/test/compress/screw-ie8.js b/test/compress/screw-ie8.js index 31c448fd..51379b15 100644 --- a/test/compress/screw-ie8.js +++ b/test/compress/screw-ie8.js @@ -17,6 +17,26 @@ dont_screw: { expect_exact: 'f("\\x0B");'; } +do_screw_constants: { + options = { + screw_ie8: true, + } + input: { + f(undefined, Infinity); + } + expect_exact: "f(void 0,1/0);" +} + +dont_screw_constants: { + options = { + screw_ie8: false, + } + input: { + f(undefined, Infinity); + } + expect_exact: "f(undefined,Infinity);" +} + do_screw_try_catch: { options = { screw_ie8: true }; mangle = { screw_ie8: true }; @@ -46,8 +66,6 @@ do_screw_try_catch: { } dont_screw_try_catch: { - // This test is known to generate incorrect code for screw_ie8=false. - // Update expected result in the event this bug is ever fixed. options = { screw_ie8: false }; mangle = { screw_ie8: false }; beautify = { screw_ie8: false }; @@ -64,11 +82,11 @@ dont_screw_try_catch: { } expect: { bad = function(n){ - return function(n){ + return function(t){ try{ - t() - } catch(t) { - n(t) + n() + } catch(n) { + t(n) } } }; @@ -104,8 +122,6 @@ do_screw_try_catch_undefined: { } dont_screw_try_catch_undefined: { - // This test is known to generate incorrect code for screw_ie8=false. - // Update expected result in the event this bug is ever fixed. options = { screw_ie8: false }; mangle = { screw_ie8: false }; beautify = { screw_ie8: false }; @@ -121,14 +137,14 @@ dont_screw_try_catch_undefined: { }; } expect: { - function a(o){ + function a(n){ try{ throw "Stuff" - } catch (n) { - console.log("caught: "+n) + } catch (undefined) { + console.log("caught: " + undefined) } - console.log("undefined is " + n); - return o === n + console.log("undefined is " + undefined); + return n === undefined } } } From b33e7f88e60f886ee3403c82ac2e3fb40caa698f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 13:09:27 +0800 Subject: [PATCH 15/53] improve `unsafe` on undefined (#1548) `unsafe` turns undefined keyword into a variable of the same name if found, but that interferes with other related optimisations. Keep track of such transformations to ensure zero information loss in the process. --- lib/compress.js | 24 +++++++++++++++--------- test/compress/issue-1443.js | 16 ++++------------ test/compress/sequences.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 4a5f6395..f2269a2f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -765,7 +765,7 @@ merge(Compressor.prototype, { CHANGED = true; stat = stat.clone(); stat.alternative = ret[0] || make_node(AST_Return, stat, { - value: make_node(AST_Undefined, stat) + value: null }); ret[0] = stat.transform(compressor); continue loop; @@ -798,7 +798,7 @@ merge(Compressor.prototype, { && !stat.alternative) { CHANGED = true; ret.push(make_node(AST_Return, ret[0], { - value: make_node(AST_Undefined, ret[0]) + value: null }).transform(compressor)); ret.unshift(stat); continue loop; @@ -1055,6 +1055,10 @@ merge(Compressor.prototype, { })); }; + function is_undefined(node) { + return node instanceof AST_Undefined || node.is_undefined; + } + /* -----[ boolean/negation helpers ]----- */ // methods to determine whether an expression has a boolean result type @@ -2402,8 +2406,8 @@ merge(Compressor.prototype, { return make_node(self.body.CTOR, self, { value: make_node(AST_Conditional, self, { condition : self.condition, - consequent : self.body.value || make_node(AST_Undefined, self.body).optimize(compressor), - alternative : self.alternative.value || make_node(AST_Undefined, self.alternative).optimize(compressor) + consequent : self.body.value || make_node(AST_Undefined, self.body), + alternative : self.alternative.value || make_node(AST_Undefined, self.alternative) }) }).transform(compressor); } @@ -2834,7 +2838,7 @@ merge(Compressor.prototype, { return self.car; } } - if (self.cdr instanceof AST_Undefined) { + if (is_undefined(self.cdr)) { return make_node(AST_UnaryPrefix, self, { operator : "void", expression : self.car @@ -2873,7 +2877,7 @@ merge(Compressor.prototype, { self.expression = e; return self; } else { - return make_node(AST_Undefined, self); + return make_node(AST_Undefined, self).transform(compressor); } } if (compressor.option("booleans") && compressor.in_boolean_context()) { @@ -3354,7 +3358,7 @@ merge(Compressor.prototype, { && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { switch (self.name) { case "undefined": - return make_node(AST_Undefined, self); + return make_node(AST_Undefined, self).transform(compressor); case "NaN": return make_node(AST_NaN, self).transform(compressor); case "Infinity": @@ -3397,11 +3401,13 @@ merge(Compressor.prototype, { var scope = compressor.find_parent(AST_Scope); var undef = scope.find_variable("undefined"); if (undef) { - return make_node(AST_SymbolRef, self, { + var ref = make_node(AST_SymbolRef, self, { name : "undefined", scope : scope, thedef : undef }); + ref.is_undefined = true; + return ref; } } return self; @@ -3688,7 +3694,7 @@ merge(Compressor.prototype, { OPT(AST_RegExp, literals_in_boolean_context); OPT(AST_Return, function(self, compressor){ - if (self.value instanceof AST_Undefined) { + if (self.value && is_undefined(self.value)) { self.value = null; } return self; diff --git a/test/compress/issue-1443.js b/test/compress/issue-1443.js index a2565872..304a71ac 100644 --- a/test/compress/issue-1443.js +++ b/test/compress/issue-1443.js @@ -2,6 +2,7 @@ unsafe_undefined: { options = { + conditionals: true, if_return: true, unsafe: true } @@ -19,12 +20,7 @@ unsafe_undefined: { expect: { function f(n) { return function() { - if (a) - return b; - if (c) - return d; - else - return n; + return a ? b : c ? d : n; }; } } @@ -32,6 +28,7 @@ unsafe_undefined: { keep_fnames: { options = { + conditionals: true, if_return: true, unsafe: true } @@ -57,12 +54,7 @@ keep_fnames: { function n(n) { return n * n; } - if (a) - return b; - if (c) - return d; - else - return r; + return a ? b : c ? d : r; }; } } diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 7bb274cb..41cfc726 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -251,3 +251,36 @@ iife: { function d() {}(), function e() {}(), function f() {}(), function g() {}(); } } + +unsafe_undefined: { + options = { + conditionals: true, + if_return: true, + sequences: true, + side_effects: true, + unsafe: true, + } + input: { + function f(undefined) { + if (a) + return b; + if (c) + return d; + } + function g(undefined) { + if (a) + return b; + if (c) + return d; + e(); + } + } + expect: { + function f(undefined) { + return a ? b : c ? d : undefined; + } + function g(undefined) { + return a ? b : c ? d : void e(); + } + } +} From b70591be1a603d3c1728e6563691c3a192023d3f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 13:13:44 +0800 Subject: [PATCH 16/53] handle variable declaration within catch blocks (#1546) accounts for IE8- scoping --- lib/ast.js | 3 --- lib/compress.js | 12 +++++++++++- lib/scope.js | 3 +-- test/compress/reduce_vars.js | 23 +++++++++++++++++++++++ test/compress/screw-ie8.js | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index f3df78fe..1f163304 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -812,9 +812,6 @@ var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { $documentation: "A declaration symbol (symbol in var/const, function name or argument, symbol in catch)", - $propdoc: { - init: "[AST_Node*/S] array of initializers for this declaration." - } }, AST_Symbol); var AST_SymbolVar = DEFNODE("SymbolVar", null, { diff --git a/lib/compress.js b/lib/compress.js index f2269a2f..1a54c75e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -223,6 +223,7 @@ merge(Compressor.prototype, { AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan){ var reduce_vars = rescan && compressor.option("reduce_vars"); + var ie8 = !compressor.option("screw_ie8"); var safe_ids = []; push(); var suppressor = new TreeWalker(function(node) { @@ -232,7 +233,7 @@ merge(Compressor.prototype, { d.fixed = false; } }); - var tw = new TreeWalker(function(node){ + var tw = new TreeWalker(function(node, descend){ if (!(node instanceof AST_Directive || node instanceof AST_Constant)) { node._squeezed = false; node._optimized = false; @@ -247,6 +248,9 @@ merge(Compressor.prototype, { d.fixed = false; } } + if (ie8 && node instanceof AST_SymbolCatch) { + node.definition().fixed = false; + } if (node instanceof AST_VarDef) { var d = node.name.definition(); if (d.fixed === undefined) { @@ -301,6 +305,12 @@ merge(Compressor.prototype, { pop(); return true; } + if (node instanceof AST_Catch) { + push(); + descend(); + pop(); + return true; + } } }); this.walk(tw); diff --git a/lib/scope.js b/lib/scope.js index b00fcb4a..f23c8fe2 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -154,8 +154,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { - var def = defun.def_variable(node); - def.init = tw.parent().value; + defun.def_variable(node); } else if (node instanceof AST_SymbolCatch) { scope.def_variable(node); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 557631bd..70e915d3 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -605,6 +605,29 @@ inner_var_for_in_2: { } } +inner_var_catch: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + try { + a(); + } catch (e) { + var b = 1; + } + console.log(b); + } + expect: { + try { + a(); + } catch (e) { + var b = 1; + } + console.log(b); + } +} + issue_1533_1: { options = { collapse_vars: true, diff --git a/test/compress/screw-ie8.js b/test/compress/screw-ie8.js index 51379b15..36eb4d3a 100644 --- a/test/compress/screw-ie8.js +++ b/test/compress/screw-ie8.js @@ -148,3 +148,37 @@ dont_screw_try_catch_undefined: { } } } + +reduce_vars: { + options = { + evaluate: true, + reduce_vars: true, + screw_ie8: false, + unused: true, + } + mangle = { + screw_ie8: false, + } + input: { + function f() { + var a; + try { + x(); + } catch (a) { + y(); + } + alert(a); + } + } + expect: { + function f() { + var t; + try { + x(); + } catch (t) { + y(); + } + alert(t); + } + } +} From 35a849dc48adf4a7318481f0ff540375ec0e43b2 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 14:56:14 +0800 Subject: [PATCH 17/53] collapse assignment with adjacent subsequent usage (#1553) - consolidate `cascade` optimisations - support ++/-- postfixes - remove redundant optimisation identified in #1460 fixes #368 --- lib/compress.js | 43 ++++++++++++++++++++++++-------------- test/compress/issue-368.js | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 test/compress/issue-368.js diff --git a/lib/compress.js b/lib/compress.js index 1a54c75e..f1409d90 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -607,10 +607,7 @@ merge(Compressor.prototype, { return statements; function is_lvalue(node, parent) { - return node instanceof AST_SymbolRef && ( - (parent instanceof AST_Assign && node === parent.left) - || (parent instanceof AST_Unary && parent.expression === node - && (parent.operator == "++" || parent.operator == "--"))); + return node instanceof AST_SymbolRef && isLHS(node, parent); } function replace_var(node, parent, is_constant) { if (is_lvalue(node, parent)) return node; @@ -1152,7 +1149,7 @@ merge(Compressor.prototype, { }); function isLHS(node, parent) { - return parent instanceof AST_Unary && (parent.operator === "++" || parent.operator === "--") + return parent instanceof AST_Unary && (parent.operator == "++" || parent.operator == "--") || parent instanceof AST_Assign && parent.left === node; } @@ -2832,20 +2829,34 @@ merge(Compressor.prototype, { self.car = self.car.drop_side_effect_free(compressor, first_in_statement(compressor)); if (!self.car) return maintain_this_binding(compressor.parent(), self, self.cdr); if (compressor.option("cascade")) { + var left; if (self.car instanceof AST_Assign && !self.car.left.has_side_effects(compressor)) { - if (self.car.left.equivalent_to(self.cdr)) { - return self.car; - } - if (self.cdr instanceof AST_Call - && self.cdr.expression.equivalent_to(self.car.left)) { - self.cdr.expression = self.car; - return self.cdr; - } + left = self.car.left; + } else if (self.car instanceof AST_UnaryPostfix + && (self.car.operator == "++" || self.car.operator == "--")) { + left = self.car.expression; } - if (!self.car.has_side_effects(compressor) - && self.car.equivalent_to(self.cdr)) { - return self.car; + if (left) { + var parent, field; + var cdr = self.cdr; + while (true) { + if (cdr.equivalent_to(left)) { + if (parent) { + parent[field] = self.car; + return self.cdr; + } + return self.car; + } + if (cdr instanceof AST_Binary && !(cdr instanceof AST_Assign)) { + field = cdr.left.is_constant() ? "right" : "left"; + } else if (cdr instanceof AST_Call + || cdr instanceof AST_Unary && cdr.operator != "++" && cdr.operator != "--") { + field = "expression"; + } else break; + parent = cdr; + cdr = cdr[field]; + } } } if (is_undefined(self.cdr)) { diff --git a/test/compress/issue-368.js b/test/compress/issue-368.js new file mode 100644 index 00000000..8c41a894 --- /dev/null +++ b/test/compress/issue-368.js @@ -0,0 +1,41 @@ +collapse: { + options = { + cascade: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + function f1() { + var a; + a = typeof b === 'function' ? b() : b; + return a !== undefined && c(); + } + function f2(b) { + var a; + b = c(); + a = typeof b === 'function' ? b() : b; + return 'stirng' == typeof a && d(); + } + function f3(c) { + var a; + a = b(a / 2); + if (a < 0) { + c++; + return c / 2; + } + } + } + expect: { + function f1() { + return void 0 !== ('function' === typeof b ? b() : b) && c(); + } + function f2(b) { + return b = c(), 'stirng' == typeof ('function' === typeof b ? b() : b) && d(); + } + function f3(c) { + var a; + if ((a = b(a / 2)) < 0) return c++ / 2; + } + } +} From 33b5f3198469f53641172c0702a7f40566325fb0 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 15:48:28 +0800 Subject: [PATCH 18/53] v2.8.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4512eb71..46f5eaec 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.5", + "version": "2.8.6", "engines": { "node": ">=0.8.0" }, From 067e5a5762cb5da8948ccb9bde4d020decb8d55b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 17:15:37 +0800 Subject: [PATCH 19/53] fixup for #1553 (#1555) - `++a` is the one that is foldable - transform `a++` into `++a` for better optimisation --- lib/compress.js | 10 +++++++--- test/compress/issue-368.js | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f1409d90..d9a67c16 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2833,7 +2833,7 @@ merge(Compressor.prototype, { if (self.car instanceof AST_Assign && !self.car.left.has_side_effects(compressor)) { left = self.car.left; - } else if (self.car instanceof AST_UnaryPostfix + } else if (self.car instanceof AST_Unary && (self.car.operator == "++" || self.car.operator == "--")) { left = self.car.expression; } @@ -2842,11 +2842,15 @@ merge(Compressor.prototype, { var cdr = self.cdr; while (true) { if (cdr.equivalent_to(left)) { + var car = self.car instanceof AST_UnaryPostfix ? make_node(AST_UnaryPrefix, self.car, { + operator: self.car.operator, + expression: left + }) : self.car; if (parent) { - parent[field] = self.car; + parent[field] = car; return self.cdr; } - return self.car; + return car; } if (cdr instanceof AST_Binary && !(cdr instanceof AST_Assign)) { field = cdr.left.is_constant() ? "right" : "left"; diff --git a/test/compress/issue-368.js b/test/compress/issue-368.js index 8c41a894..5960aa64 100644 --- a/test/compress/issue-368.js +++ b/test/compress/issue-368.js @@ -21,6 +21,16 @@ collapse: { var a; a = b(a / 2); if (a < 0) { + a++; + ++c; + return c / 2; + } + } + function f4(c) { + var a; + a = b(a / 2); + if (a < 0) { + a++; c++; return c / 2; } @@ -35,7 +45,11 @@ collapse: { } function f3(c) { var a; - if ((a = b(a / 2)) < 0) return c++ / 2; + if ((a = b(a / 2)) < 0) return a++, ++c / 2; + } + function f4(c) { + var a; + if ((a = b(a / 2)) < 0) return a++, ++c / 2; } } } From a5d62a3fc6dcb334a2172bfbfdc4e25efe4367da Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Mar 2017 17:17:08 +0800 Subject: [PATCH 20/53] v2.8.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46f5eaec..31aaf951 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.6", + "version": "2.8.7", "engines": { "node": ">=0.8.0" }, From a9fc9ddc3371a2ba051bca849db3c4b73983f24d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 6 Mar 2017 17:31:35 +0800 Subject: [PATCH 21/53] suppress semicolons after do/while (#1556) - unless both `beautify` & `screw-ie8` are enabled - deprecate workaround for if-do-while-else fixes #186 --- lib/output.js | 13 +--- test/compress/loops.js | 156 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 10 deletions(-) diff --git a/lib/output.js b/lib/output.js index 10fed135..7e16644d 100644 --- a/lib/output.js +++ b/lib/output.js @@ -784,7 +784,9 @@ function OutputStream(options) { output.with_parens(function(){ self.condition.print(output); }); - output.semicolon(); + if (output.option("beautify") && output.option("screw_ie8")) { + output.semicolon(); + } }); DEFPRINT(AST_While, function(self, output){ output.print("while"); @@ -917,15 +919,6 @@ function OutputStream(options) { // adds the block brackets if needed. if (!self.body) return output.force_semicolon(); - if (self.body instanceof AST_Do) { - // Unconditionally use the if/do-while workaround for all browsers. - // https://github.com/mishoo/UglifyJS/issues/#issue/57 IE - // croaks with "syntax error" on code like this: if (foo) - // do ... while(cond); else ... we need block brackets - // around do/while - make_block(self.body, output); - return; - } var b = self.body; while (true) { if (b instanceof AST_If) { diff --git a/test/compress/loops.js b/test/compress/loops.js index e26dc79f..2d04e235 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -240,3 +240,159 @@ issue_1532: { } } } + +issue_186: { + beautify = { + beautify: false, + screw_ie8: true, + } + input: { + var x = 3; + if (foo()) + do + do + alert(x); + while (--x); + while (x); + else + bar(); + } + expect_exact: 'var x=3;if(foo())do do alert(x);while(--x)while(x)else bar();' +} + +issue_186_ie8: { + beautify = { + beautify: false, + screw_ie8: false, + } + input: { + var x = 3; + if (foo()) + do + do + alert(x); + while (--x); + while (x); + else + bar(); + } + expect_exact: 'var x=3;if(foo())do do alert(x);while(--x)while(x)else bar();' +} + +issue_186_beautify: { + beautify = { + beautify: true, + screw_ie8: true, + } + input: { + var x = 3; + if (foo()) + do + do + alert(x); + while (--x); + while (x); + else + bar(); + } + expect_exact: 'var x = 3;\n\nif (foo()) do do alert(x); while (--x); while (x); else bar();' +} + +issue_186_beautify_ie8: { + beautify = { + beautify: true, + screw_ie8: false, + } + input: { + var x = 3; + if (foo()) + do + do + alert(x); + while (--x); + while (x); + else + bar(); + } + expect_exact: 'var x = 3;\n\nif (foo()) do do alert(x); while (--x) while (x) else bar();' +} + +issue_186_bracketize: { + beautify = { + beautify: false, + bracketize: true, + screw_ie8: true, + } + input: { + var x = 3; + if (foo()) + do + do + alert(x); + while (--x); + while (x); + else + bar(); + } + expect_exact: 'var x=3;if(foo()){do{do{alert(x)}while(--x)}while(x)}else{bar()}' +} + +issue_186_bracketize_ie8: { + beautify = { + beautify: false, + bracketize: true, + screw_ie8: false, + } + input: { + var x = 3; + if (foo()) + do + do + alert(x); + while (--x); + while (x); + else + bar(); + } + expect_exact: 'var x=3;if(foo()){do{do{alert(x)}while(--x)}while(x)}else{bar()}' +} + +issue_186_beautify_bracketize: { + beautify = { + beautify: true, + bracketize: true, + screw_ie8: true, + } + input: { + var x = 3; + if (foo()) + do + do + alert(x); + while (--x); + while (x); + else + bar(); + } + expect_exact: 'var x = 3;\n\nif (foo()) {\n do {\n do {\n alert(x);\n } while (--x);\n } while (x);\n} else {\n bar();\n}' +} + +issue_186_beautify_bracketize_ie8: { + beautify = { + beautify: true, + bracketize: true, + screw_ie8: false, + } + input: { + var x = 3; + if (foo()) + do + do + alert(x); + while (--x); + while (x); + else + bar(); + } + expect_exact: 'var x = 3;\n\nif (foo()) {\n do {\n do {\n alert(x);\n } while (--x)\n } while (x)\n} else {\n bar();\n}' +} From 3ac24219322384539caae803482ea257e7971cf2 Mon Sep 17 00:00:00 2001 From: kzc Date: Mon, 6 Mar 2017 12:42:33 -0500 Subject: [PATCH 22/53] collapse_vars: do not replace a constant in loop condition or init (#1562) --- lib/compress.js | 9 ++++-- test/compress/collapse_vars.js | 50 ++++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index d9a67c16..8c3fb155 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -535,10 +535,13 @@ merge(Compressor.prototype, { // Constant single use vars can be replaced in any scope. if (var_decl.value.is_constant()) { var ctt = new TreeTransformer(function(node) { - if (node === ref - && !ctt.find_parent(AST_ForIn)) { - return replace_var(node, ctt.parent(), true); + var parent = ctt.parent(); + if (parent instanceof AST_IterationStatement + && (parent.condition === node || parent.init === node)) { + return node; } + if (node === ref) + return replace_var(node, parent, true); }); stat.transform(ctt); continue; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 82d943ff..6d7e2d9f 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -344,9 +344,9 @@ collapse_vars_do_while: { } input: { function f1(y) { - // The constant do-while condition `c` will be replaced. + // The constant do-while condition `c` will not be replaced. var c = 9; - do { } while (c === 77); + do {} while (c === 77); } function f2(y) { // The non-constant do-while condition `c` will not be replaced. @@ -381,7 +381,8 @@ collapse_vars_do_while: { } expect: { function f1(y) { - do ; while (false); + var c = 9; + do ; while (77 === c); } function f2(y) { var c = 5 - y; @@ -418,9 +419,9 @@ collapse_vars_do_while_drop_assign: { } input: { function f1(y) { - // The constant do-while condition `c` will be replaced. + // The constant do-while condition `c` will be not replaced. var c = 9; - do { } while (c === 77); + do {} while (c === 77); } function f2(y) { // The non-constant do-while condition `c` will not be replaced. @@ -455,7 +456,8 @@ collapse_vars_do_while_drop_assign: { } expect: { function f1(y) { - do ; while (false); + var c = 9; + do ; while (77 === c); } function f2(y) { var c = 5 - y; @@ -1309,8 +1311,8 @@ collapse_vars_regexp: { }; } (function(){ - var result, rx = /ab*/g; - while (result = rx.exec('acdabcdeabbb')) + var result, s = "acdabcdeabbb", rx = /ab*/g; + while (result = rx.exec(s)) console.log(result[0]); })(); } @@ -1329,3 +1331,35 @@ issue_1537: { for (k in {prop: 'val'}); } } + +issue_1562: { + options = { + collapse_vars: true, + } + input: { + var v = 1, B = 2; + for (v in objs) f(B); + + var x = 3, C = 10; + while(x + 2) bar(C); + + var y = 4, D = 20; + do bar(D); while(y + 2); + + var z = 5, E = 30; + for (; f(z + 2) ;) bar(E); + } + expect: { + var v = 1; + for (v in objs) f(2); + + var x = 3; + while(x + 2) bar(10); + + var y = 4; + do bar(20); while(y + 2); + + var z = 5; + for (; f(z + 2) ;) bar(30); + } +} From d787d70127af1b8df57eee2274c720d34092aef2 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 7 Mar 2017 03:11:03 +0800 Subject: [PATCH 23/53] avoid substitution of global variables (#1557) - unless `toplevel` is enabled - global `const` works as before --- lib/compress.js | 7 +- test/compress/dead-code.js | 2 + test/compress/reduce_vars.js | 390 ++++++++++++++++++++++++++--------- 3 files changed, 306 insertions(+), 93 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 8c3fb155..696e2056 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -223,6 +223,7 @@ merge(Compressor.prototype, { AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan){ var reduce_vars = rescan && compressor.option("reduce_vars"); + var toplevel = compressor.option("toplevel"); var ie8 = !compressor.option("screw_ie8"); var safe_ids = []; push(); @@ -334,7 +335,11 @@ merge(Compressor.prototype, { } function reset_def(def) { - def.fixed = undefined; + if (toplevel || !def.global || def.orig[0] instanceof AST_SymbolConst) { + def.fixed = undefined; + } else { + def.fixed = false; + } def.references = []; def.should_replace = undefined; } diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index cd96d02d..45b7af6e 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -123,6 +123,7 @@ dead_code_const_annotation: { conditionals : true, evaluate : true, reduce_vars : true, + toplevel : true, }; input: { var unused; @@ -172,6 +173,7 @@ dead_code_const_annotation_complex_scope: { conditionals : true, evaluate : true, reduce_vars : true, + toplevel : true, }; input: { var unused_var; diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 70e915d3..27f77fb5 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -6,6 +6,7 @@ reduce_vars: { C : 0 }, reduce_vars : true, + toplevel : true, unused : true } input: { @@ -452,22 +453,26 @@ multi_def_2: { reduce_vars: true, } input: { - if (code == 16) - var bitsLength = 2, bitsOffset = 3, what = len; - else if (code == 17) - var bitsLength = 3, bitsOffset = 3, what = (len = 0); - else if (code == 18) - var bitsLength = 7, bitsOffset = 11, what = (len = 0); - var repeatLength = this.getBits(bitsLength) + bitsOffset; + function f(){ + if (code == 16) + var bitsLength = 2, bitsOffset = 3, what = len; + else if (code == 17) + var bitsLength = 3, bitsOffset = 3, what = (len = 0); + else if (code == 18) + var bitsLength = 7, bitsOffset = 11, what = (len = 0); + var repeatLength = this.getBits(bitsLength) + bitsOffset; + } } expect: { - if (16 == code) - var bitsLength = 2, bitsOffset = 3, what = len; - else if (17 == code) - var bitsLength = 3, bitsOffset = 3, what = (len = 0); - else if (18 == code) - var bitsLength = 7, bitsOffset = 11, what = (len = 0); - var repeatLength = this.getBits(bitsLength) + bitsOffset; + function f(){ + if (16 == code) + var bitsLength = 2, bitsOffset = 3, what = len; + else if (17 == code) + var bitsLength = 3, bitsOffset = 3, what = (len = 0); + else if (18 == code) + var bitsLength = 7, bitsOffset = 11, what = (len = 0); + var repeatLength = this.getBits(bitsLength) + bitsOffset; + } } } @@ -477,12 +482,16 @@ use_before_var: { reduce_vars: true, } input: { - console.log(t); - var t = 1; + function f(){ + console.log(t); + var t = 1; + } } expect: { - console.log(t); - var t = 1; + function f(){ + console.log(t); + var t = 1; + } } } @@ -492,22 +501,20 @@ inner_var_if: { reduce_vars: true, } input: { - function f(){ - return 0; + function f(a){ + if (a) + var t = 1; + if (!t) + console.log(t); } - if (f()) - var t = 1; - if (!t) - console.log(t); } expect: { - function f(){ - return 0; + function f(a){ + if (a) + var t = 1; + if (!t) + console.log(t); } - if (f()) - var t = 1; - if (!t) - console.log(t); } } @@ -517,24 +524,22 @@ inner_var_label: { reduce_vars: true, } input: { - function f(){ - return 1; + function f(a){ + l: { + if (a) break l; + var t = 1; + } + console.log(t); } - l: { - if (f()) break l; - var t = 1; - } - console.log(t); } expect: { - function f(){ - return 1; + function f(a){ + l: { + if (a) break l; + var t = 1; + } + console.log(t); } - l: { - if (f()) break l; - var t = 1; - } - console.log(t); } } @@ -544,22 +549,26 @@ inner_var_for: { reduce_vars: true, } input: { - var a = 1; - x(a, b, d); - for (var b = 2, c = 3; x(a, b, c, d); x(a, b, c, d)) { - var d = 4, e = 5; + function f() { + var a = 1; + x(a, b, d); + for (var b = 2, c = 3; x(a, b, c, d); x(a, b, c, d)) { + var d = 4, e = 5; + x(a, b, c, d, e); + } x(a, b, c, d, e); } - x(a, b, c, d, e) } expect: { - var a = 1; - x(1, b, d); - for (var b = 2, c = 3; x(1, b, 3, d); x(1, b, 3, d)) { - var d = 4, e = 5; + function f() { + var a = 1; + x(1, b, d); + for (var b = 2, c = 3; x(1, b, 3, d); x(1, b, 3, d)) { + var d = 4, e = 5; + x(1, b, 3, d, e); + } x(1, b, 3, d, e); } - x(1, b, 3, d, e); } } @@ -569,24 +578,28 @@ inner_var_for_in_1: { reduce_vars: true, } input: { - var a = 1, b = 2; - for (b in (function() { - return x(a, b, c); - })()) { - var c = 3, d = 4; + function f() { + var a = 1, b = 2; + for (b in (function() { + return x(a, b, c); + })()) { + var c = 3, d = 4; + x(a, b, c, d); + } x(a, b, c, d); } - x(a, b, c, d); } expect: { - var a = 1, b = 2; - for (b in (function() { - return x(1, b, c); - })()) { - var c = 3, d = 4; + function f() { + var a = 1, b = 2; + for (b in (function() { + return x(1, b, c); + })()) { + var c = 3, d = 4; + x(1, b, c, d); + } x(1, b, c, d); } - x(1, b, c, d); } } @@ -596,12 +609,16 @@ inner_var_for_in_2: { reduce_vars: true, } input: { - for (var long_name in {}) - console.log(long_name); + function f() { + for (var long_name in {}) + console.log(long_name); + } } expect: { - for (var long_name in {}) - console.log(long_name); + function f() { + for (var long_name in {}) + console.log(long_name); + } } } @@ -611,20 +628,24 @@ inner_var_catch: { reduce_vars: true, } input: { - try { - a(); - } catch (e) { - var b = 1; + function f() { + try { + a(); + } catch (e) { + var b = 1; + } + console.log(b); } - console.log(b); } expect: { - try { - a(); - } catch (e) { - var b = 1; + function f() { + try { + a(); + } catch (e) { + var b = 1; + } + console.log(b); } - console.log(b); } } @@ -634,14 +655,18 @@ issue_1533_1: { reduce_vars: true, } input: { - var id = ""; - for (id in {break: "me"}) - console.log(id); + function f() { + var id = ""; + for (id in {break: "me"}) + console.log(id); + } } expect: { - var id = ""; - for (id in {break: "me"}) - console.log(id); + function f() { + var id = ""; + for (id in {break: "me"}) + console.log(id); + } } } @@ -651,15 +676,196 @@ issue_1533_2: { reduce_vars: true, } input: { - var id = ""; - for (var id in {break: "me"}) + function f() { + var id = ""; + for (var id in {break: "me"}) + console.log(id); console.log(id); - console.log(id); + } } expect: { - var id = ""; - for (var id in {break: "me"}) + function f() { + var id = ""; + for (var id in {break: "me"}) + console.log(id); console.log(id); - console.log(id); + } + } +} + +toplevel_on: { + options = { + evaluate: true, + reduce_vars: true, + toplevel:true, + unused: true, + } + input: { + var x = 3; + console.log(x); + } + expect: { + console.log(3); + } +} + +toplevel_off: { + options = { + evaluate: true, + reduce_vars: true, + toplevel:false, + unused: true, + } + input: { + var x = 3; + console.log(x); + } + expect: { + var x = 3; + console.log(x); + } +} + +toplevel_on_loops_1: { + options = { + evaluate: true, + loops: true, + reduce_vars: true, + toplevel:true, + unused: true, + } + input: { + function bar() { + console.log("bar:", --x); + } + var x = 3; + do + bar(); + while (x); + } + expect: { + function bar() { + console.log("bar:", --x); + } + var x = 3; + do + bar(); + while (x); + } +} + +toplevel_off_loops_1: { + options = { + evaluate: true, + loops: true, + reduce_vars: true, + toplevel:false, + unused: true, + } + input: { + function bar() { + console.log("bar:", --x); + } + var x = 3; + do + bar(); + while (x); + } + expect: { + function bar() { + console.log("bar:", --x); + } + var x = 3; + do + bar(); + while (x); + } +} + +toplevel_on_loops_2: { + options = { + evaluate: true, + loops: true, + reduce_vars: true, + toplevel:true, + unused: true, + } + input: { + function bar() { + console.log("bar:"); + } + var x = 3; + do + bar(); + while (x); + } + expect: { + function bar() { + console.log("bar:"); + } + for (;;) bar(); + } +} + +toplevel_off_loops_2: { + options = { + evaluate: true, + loops: true, + reduce_vars: true, + toplevel:false, + unused: true, + } + input: { + function bar() { + console.log("bar:"); + } + var x = 3; + do + bar(); + while (x); + } + expect: { + function bar() { + console.log("bar:"); + } + var x = 3; + do + bar(); + while (x); + } +} + +toplevel_on_loops_3: { + options = { + evaluate: true, + loops: true, + reduce_vars: true, + toplevel:true, + unused: true, + } + input: { + var x = 3; + while (x) bar(); + } + expect: { + for (;;) bar(); + } +} + +toplevel_off_loops_3: { + options = { + evaluate: true, + loops: true, + reduce_vars: true, + toplevel:false, + unused: true, + } + input: { + var x = 3; + while (x) bar(); + } + expect: { + var x = 3; + for (;x;) bar(); } } From 8153b7bd8a70ad94666904bd41f12ebd6be684c8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 7 Mar 2017 15:37:52 +0800 Subject: [PATCH 24/53] transform function calls to IIFEs (#1560) - expose function body to call sites for potential optimisations - suppress substitution of variable used within `AST_Defun` --- lib/ast.js | 10 +- lib/compress.js | 51 ++++++-- test/compress/reduce_vars.js | 238 +++++++++++++++++++++++++++++++++-- test/mocha/cli.js | 2 +- test/mocha/glob.js | 12 +- 5 files changed, 284 insertions(+), 29 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 1f163304..a2125e70 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -91,7 +91,15 @@ var AST_Token = DEFNODE("Token", "type value line col pos endline endcol endpos }, null); var AST_Node = DEFNODE("Node", "start end", { - clone: function() { + clone: function(deep) { + if (deep) { + var self = this.clone(); + return self.transform(new TreeTransformer(function(node) { + if (node !== self) { + return node.clone(true); + } + })); + } return new this.CTOR(this); }, $documentation: "Base class of all AST nodes", diff --git a/lib/compress.js b/lib/compress.js index 696e2056..8bbbc3f6 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -245,7 +245,8 @@ merge(Compressor.prototype, { if (node instanceof AST_SymbolRef) { var d = node.definition(); d.references.push(node); - if (!d.fixed || isModified(node, 0) || !is_safe(d)) { + if (!d.fixed || !is_safe(d) + || is_modified(node, 0, d.fixed instanceof AST_Lambda)) { d.fixed = false; } } @@ -261,6 +262,21 @@ merge(Compressor.prototype, { d.fixed = false; } } + if (node instanceof AST_Defun) { + var d = node.name.definition(); + if (!toplevel && d.global || is_safe(d)) { + d.fixed = false; + } else { + d.fixed = node; + mark_as_safe(d); + } + var save_ids = safe_ids; + safe_ids = []; + push(); + descend(); + safe_ids = save_ids; + return true; + } var iife; if (node instanceof AST_Function && (iife = tw.parent()) instanceof AST_Call @@ -344,13 +360,13 @@ merge(Compressor.prototype, { def.should_replace = undefined; } - function isModified(node, level) { + function is_modified(node, level, func) { var parent = tw.parent(level); if (isLHS(node, parent) - || parent instanceof AST_Call && parent.expression === node) { + || !func && parent instanceof AST_Call && parent.expression === node) { return true; } else if (parent instanceof AST_PropAccess && parent.expression === node) { - return isModified(parent, level + 1); + return !func && is_modified(parent, level + 1); } } }); @@ -1307,14 +1323,7 @@ merge(Compressor.prototype, { def(AST_Statement, function(){ throw new Error(string_template("Cannot evaluate a statement [{file}:{line},{col}]", this.start)); }); - // XXX: AST_Accessor and AST_Function both inherit from AST_Scope, - // which itself inherits from AST_Statement; however, they aren't - // really statements. This could bite in other places too. :-( - // Wish JS had multiple inheritance. - def(AST_Accessor, function(){ - throw def; - }); - def(AST_Function, function(){ + def(AST_Lambda, function(){ throw def; }); function ev(node, compressor) { @@ -2590,6 +2599,24 @@ merge(Compressor.prototype, { OPT(AST_Call, function(self, compressor){ var exp = self.expression; + if (compressor.option("reduce_vars") + && exp instanceof AST_SymbolRef) { + var def = exp.definition(); + if (def.fixed instanceof AST_Defun) { + def.fixed = make_node(AST_Function, def.fixed, def.fixed).clone(true); + } + if (def.fixed instanceof AST_Function) { + exp = def.fixed; + if (compressor.option("unused") + && def.references.length == 1 + && compressor.find_parent(AST_Scope) === def.scope) { + if (!compressor.option("keep_fnames")) { + exp.name = null; + } + self.expression = exp; + } + } + } if (compressor.option("unused") && exp instanceof AST_Function && !exp.uses_arguments diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 27f77fb5..a373de29 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -744,12 +744,11 @@ toplevel_on_loops_1: { while (x); } expect: { - function bar() { - console.log("bar:", --x); - } var x = 3; do - bar(); + (function() { + console.log("bar:", --x); + })(); while (x); } } @@ -800,10 +799,9 @@ toplevel_on_loops_2: { while (x); } expect: { - function bar() { + for (;;) (function() { console.log("bar:"); - } - for (;;) bar(); + })(); } } @@ -869,3 +867,229 @@ toplevel_off_loops_3: { for (;x;) bar(); } } + +defun_reference: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + function f() { + function g() { + x(); + return a; + } + var a = h(); + var b = 2; + return a + b; + function h() { + y(); + return b; + } + } + } + expect: { + function f() { + function g() { + x(); + return a; + } + var a = h(); + var b = 2; + return a + b; + function h() { + y(); + return b; + } + } + } +} + +defun_inline_1: { + options = { + reduce_vars: true, + unused: true, + } + input: { + function f() { + return g(2) + h(); + function g(b) { + return b; + } + function h() { + return h(); + } + } + } + expect: { + function f() { + return function(b) { + return b; + }(2) + h(); + function h() { + return h(); + } + } + } +} + +defun_inline_2: { + options = { + reduce_vars: true, + unused: true, + } + input: { + function f() { + function g(b) { + return b; + } + function h() { + return h(); + } + return g(2) + h(); + } + } + expect: { + function f() { + function h() { + return h(); + } + return function(b) { + return b; + }(2) + h(); + } + } +} + +defun_inline_3: { + options = { + evaluate: true, + passes: 2, + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f() { + return g(2); + function g(b) { + return b; + } + } + } + expect: { + function f() { + return 2; + } + } +} + +defun_call: { + options = { + reduce_vars: true, + unused: true, + } + input: { + function f() { + return g() + h(1) - h(g(), 2, 3); + function g() { + return 4; + } + function h(a) { + return a; + } + } + } + expect: { + function f() { + return 4 + h(1) - h(4); + function h(a) { + return a; + } + } + } +} + +defun_redefine: { + options = { + reduce_vars: true, + unused: true, + } + input: { + function f() { + function g() { + return 1; + } + function h() { + return 2; + } + g = function() { + return 3; + }; + return g() + h(); + } + } + expect: { + function f() { + function g() { + return 1; + } + g = function() { + return 3; + }; + return g() + 2; + } + } +} + +func_inline: { + options = { + reduce_vars: true, + unused: true, + } + input: { + function f() { + var g = function() { + return 1; + }; + console.log(g() + h()); + var h = function() { + return 2; + }; + } + } + expect: { + function f() { + console.log(1 + h()); + var h = function() { + return 2; + }; + } + } +} + +func_modified: { + options = { + reduce_vars: true, + unused: true, + } + input: { + function f(a) { + function a() { return 1; } + function b() { return 2; } + function c() { return 3; } + b.inject = []; + c = function() { return 4; }; + return a() + b() + c(); + } + } + expect: { + function f(a) { + function b() { return 2; } + function c() { return 3; } + b.inject = []; + c = function() { return 4; }; + return 1 + 2 + c(); + } + } +} diff --git a/test/mocha/cli.js b/test/mocha/cli.js index c07eeee7..e8e07cb5 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -82,7 +82,7 @@ describe("bin/uglifyjs", function () { }); }); it("Should work with --keep-fnames (mangle & compress)", function (done) { - var command = uglifyjscmd + ' test/input/issue-1431/sample.js --keep-fnames -m -c'; + var command = uglifyjscmd + ' test/input/issue-1431/sample.js --keep-fnames -m -c unused=false'; exec(command, function (err, stdout) { if (err) throw err; diff --git a/test/mocha/glob.js b/test/mocha/glob.js index c2fc9464..30313656 100644 --- a/test/mocha/glob.js +++ b/test/mocha/glob.js @@ -3,17 +3,13 @@ var assert = require("assert"); describe("minify() with input file globs", function() { it("minify() with one input file glob string.", function() { - var result = Uglify.minify("test/input/issue-1242/foo.*", { - compress: { collapse_vars: true } - }); + var result = Uglify.minify("test/input/issue-1242/foo.*"); assert.strictEqual(result.code, 'function foo(o){print("Foo:",2*o)}var print=console.log.bind(console);'); }); it("minify() with an array of one input file glob.", function() { var result = Uglify.minify([ "test/input/issue-1242/b*.es5", - ], { - compress: { collapse_vars: true } - }); + ]); assert.strictEqual(result.code, 'function bar(n){return 3*n}function baz(n){return n/2}'); }); it("minify() with an array of multiple input file globs.", function() { @@ -21,8 +17,8 @@ describe("minify() with input file globs", function() { "test/input/issue-1242/???.es5", "test/input/issue-1242/*.js", ], { - compress: { collapse_vars: true } + compress: { toplevel: true } }); - assert.strictEqual(result.code, 'function bar(n){return 3*n}function baz(n){return n/2}function foo(n){print("Foo:",2*n)}var print=console.log.bind(console);print("qux",bar(3),baz(12)),foo(11);'); + assert.strictEqual(result.code, 'var print=console.log.bind(console);print("qux",function(n){return 3*n}(3),function(n){return n/2}(12)),function(n){print("Foo:",2*n)}(11);'); }); }); From 8a8a94a596f57981e2cc65ac37921b154a9992b7 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 7 Mar 2017 18:38:27 +0800 Subject: [PATCH 25/53] fix deep cloning of labels (#1565) `AST_Label.references` get `.initialize()` to `[]` every time after `.clone()` So walk down the tree to pick up the cloned `AST_LoopControl` pieces and put it back together. --- lib/ast.js | 19 ++++++++++++++++++- test/compress/reduce_vars.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index a2125e70..f7ab52e2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -91,7 +91,7 @@ var AST_Token = DEFNODE("Token", "type value line col pos endline endcol endpos }, null); var AST_Node = DEFNODE("Node", "start end", { - clone: function(deep) { + _clone: function(deep) { if (deep) { var self = this.clone(); return self.transform(new TreeTransformer(function(node) { @@ -102,6 +102,9 @@ var AST_Node = DEFNODE("Node", "start end", { } return new this.CTOR(this); }, + clone: function(deep) { + return this._clone(deep); + }, $documentation: "Base class of all AST nodes", $propdoc: { start: "[AST_Token] The first token of this node", @@ -207,6 +210,20 @@ var AST_LabeledStatement = DEFNODE("LabeledStatement", "label", { this.label._walk(visitor); this.body._walk(visitor); }); + }, + clone: function(deep) { + var node = this._clone(deep); + if (deep) { + var refs = node.label.references; + var label = this.label; + node.walk(new TreeWalker(function(node) { + if (node instanceof AST_LoopControl + && node.label && node.label.thedef === label) { + refs.push(node); + } + })); + } + return node; } }, AST_StatementWithBody); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index a373de29..53e28152 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1093,3 +1093,32 @@ func_modified: { } } } + +defun_label: { + options = { + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + !function() { + function f(a) { + L: { + if (a) break L; + return 1; + } + } + console.log(f(2)); + }(); + } + expect: { + !function() { + console.log(function(a) { + L: { + if (a) break L; + return 1; + } + }(2)); + }(); + } +} From 65c848cc6f13cb5ae3bc1c0fb5fbb320ed2d0200 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 7 Mar 2017 19:25:12 +0800 Subject: [PATCH 26/53] include benchmark.js in test suite (#1564) - report file sizes and overall run time - exit with non-zero code upon error --- test/benchmark.js | 45 ++++++++++++++++++++++++++++++++++------- test/mocha/benchmark.js | 24 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 test/mocha/benchmark.js diff --git a/test/benchmark.js b/test/benchmark.js index dc176a88..c150e5cf 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -24,26 +24,57 @@ var results = {}; var remaining = 2 * urls.length; function done() { if (!--remaining) { + var failures = []; urls.forEach(function(url) { + var info = results[url]; console.log(); console.log(url); - console.log(results[url].time); - console.log("SHA1:", results[url].sha1); + console.log(info.log); + var elapsed = 0; + info.log.replace(/: ([0-9]+\.[0-9]{3})s/g, function(match, time) { + elapsed += parseFloat(time); + }); + console.log("Run-time:", elapsed.toFixed(3), "s"); + console.log("Original:", info.input, "bytes"); + console.log("Uglified:", info.output, "bytes"); + console.log("SHA1 sum:", info.sha1); + if (info.code) { + failures.push(url); + } }); + if (failures.length) { + console.error("Benchmark failed:"); + failures.forEach(function(url) { + console.error(url); + }); + process.exit(1); + } } } urls.forEach(function(url) { - results[url] = { time: "" }; + results[url] = { + input: 0, + output: 0, + log: "" + }; require(url.slice(0, url.indexOf(":"))).get(url, function(res) { var uglifyjs = fork("bin/uglifyjs", args, { silent: true }); - res.pipe(uglifyjs.stdin); - uglifyjs.stdout.pipe(createHash("sha1")).on("data", function(data) { + res.on("data", function(data) { + results[url].input += data.length; + }).pipe(uglifyjs.stdin); + uglifyjs.stdout.on("data", function(data) { + results[url].output += data.length; + }).pipe(createHash("sha1")).on("data", function(data) { results[url].sha1 = data.toString("hex"); done(); }); uglifyjs.stderr.setEncoding("utf8"); uglifyjs.stderr.on("data", function(data) { - results[url].time += data; - }).on("end", done) + results[url].log += data; + }); + uglifyjs.on("exit", function(code) { + results[url].code = code; + done(); + }); }); }); diff --git a/test/mocha/benchmark.js b/test/mocha/benchmark.js new file mode 100644 index 00000000..c2c7c02c --- /dev/null +++ b/test/mocha/benchmark.js @@ -0,0 +1,24 @@ +var assert = require("assert"); +var exec = require("child_process").exec; + +describe("test/benchmark.js", function() { + this.timeout(120000); + var command = '"' + process.argv[0] + '" test/benchmark.js '; + [ + "-b", + "-b bracketize", + "-m", + "-mc passes=3", + "-mc passes=3,toplevel", + "-mc passes=3,unsafe", + "-mc keep_fargs=false,passes=3", + "-mc keep_fargs=false,passes=3,pure_getters,unsafe,unsafe_comps,unsafe_math,unsafe_proto", + ].forEach(function(args) { + it("Should pass with options " + args, function(done) { + exec(command + args, function(err) { + if (err) throw err; + done(); + }); + }); + }); +}); From 144052ca491144c65adc9d081b17ce100e3de59d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 7 Mar 2017 19:58:41 +0800 Subject: [PATCH 27/53] v2.8.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31aaf951..086b6910 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.7", + "version": "2.8.8", "engines": { "node": ">=0.8.0" }, From bd6dee52abd607244d89f606a729e811a9b0b478 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 8 Mar 2017 03:31:51 +0800 Subject: [PATCH 28/53] fix return from recursive IIFE (#1570) `side-effects` did not account for IIFEs being able to reference itself thus making its return value potentially significant --- lib/compress.js | 3 ++- test/compress/issue-1569.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1569.js diff --git a/lib/compress.js b/lib/compress.js index 8bbbc3f6..85b457e3 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2100,7 +2100,8 @@ merge(Compressor.prototype, { def(AST_This, return_null); def(AST_Call, function(compressor, first_in_statement){ if (!this.has_pure_annotation(compressor) && compressor.pure_funcs(this)) { - if (this.expression instanceof AST_Function) { + if (this.expression instanceof AST_Function + && (!this.expression.name || !this.expression.name.definition().references.length)) { var node = this.clone(); node.expression = node.expression.process_expression(false); return node; diff --git a/test/compress/issue-1569.js b/test/compress/issue-1569.js new file mode 100644 index 00000000..5f0bca34 --- /dev/null +++ b/test/compress/issue-1569.js @@ -0,0 +1,19 @@ +inner_reference: { + options = { + side_effects: true, + } + input: { + !function f(a) { + return a && f(a - 1) + a; + }(42); + !function g(a) { + return a; + }(42); + } + expect: { + !function f(a) { + return a && f(a - 1) + a; + }(42); + !void 0; + } +} From dedbeeff15b53a8ab79f9d477ac414e9a6c1ef16 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 8 Mar 2017 05:07:05 +0800 Subject: [PATCH 29/53] plan B for IE8 do-while semi-colon fix (#1572) - omitting trailing semi-colon in do-while breaks non-browser parser, e.g. uglify-js 1.x - trailing semi-colon only breaks IE8 if followed by `else` or `while` - always use braces in do-while body to workaround 2nd case with no size loss in compression fixes #1568 --- lib/output.js | 36 ++++++++++++------------------------ test/compress/loops.js | 10 +++++----- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/lib/output.js b/lib/output.js index 7e16644d..767abd4d 100644 --- a/lib/output.js +++ b/lib/output.js @@ -777,16 +777,14 @@ function OutputStream(options) { DEFPRINT(AST_Do, function(self, output){ output.print("do"); output.space(); - self._do_print_body(output); + make_block(self.body, output); output.space(); output.print("while"); output.space(); output.with_parens(function(){ self.condition.print(output); }); - if (output.option("beautify") && output.option("screw_ie8")) { - output.semicolon(); - } + output.semicolon(); }); DEFPRINT(AST_While, function(self, output){ output.print("while"); @@ -906,10 +904,10 @@ function OutputStream(options) { /* -----[ if ]----- */ function make_then(self, output) { - if (output.option("bracketize")) { - make_block(self.body, output); - return; - } + var b = self.body; + if (output.option("bracketize") + || !output.option("screw_ie8") && b instanceof AST_Do) + return make_block(b, output); // The squeezer replaces "block"-s that contain only a single // statement with the statement itself; technically, the AST // is correct, but this can create problems when we output an @@ -917,9 +915,7 @@ function OutputStream(options) { // IF *without* an ELSE block (then the outer ELSE would refer // to the inner IF). This function checks for this case and // adds the block brackets if needed. - if (!self.body) - return output.force_semicolon(); - var b = self.body; + if (!b) return output.force_semicolon(); while (true) { if (b instanceof AST_If) { if (!b.alternative) { @@ -1328,15 +1324,7 @@ function OutputStream(options) { function force_statement(stat, output) { if (output.option("bracketize")) { - if (!stat || stat instanceof AST_EmptyStatement) - output.print("{}"); - else if (stat instanceof AST_BlockStatement) - stat.print(output); - else output.with_block(function(){ - output.indent(); - stat.print(output); - output.newline(); - }); + make_block(stat, output); } else { if (!stat || stat instanceof AST_EmptyStatement) output.force_semicolon(); @@ -1385,11 +1373,11 @@ function OutputStream(options) { }; function make_block(stmt, output) { - if (stmt instanceof AST_BlockStatement) { + if (!stmt || stmt instanceof AST_EmptyStatement) + output.print("{}"); + else if (stmt instanceof AST_BlockStatement) stmt.print(output); - return; - } - output.with_block(function(){ + else output.with_block(function(){ output.indent(); stmt.print(output); output.newline(); diff --git a/test/compress/loops.js b/test/compress/loops.js index 2d04e235..df3011cd 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -257,7 +257,7 @@ issue_186: { else bar(); } - expect_exact: 'var x=3;if(foo())do do alert(x);while(--x)while(x)else bar();' + expect_exact: 'var x=3;if(foo())do{do{alert(x)}while(--x)}while(x);else bar();' } issue_186_ie8: { @@ -276,7 +276,7 @@ issue_186_ie8: { else bar(); } - expect_exact: 'var x=3;if(foo())do do alert(x);while(--x)while(x)else bar();' + expect_exact: 'var x=3;if(foo()){do{do{alert(x)}while(--x)}while(x)}else bar();' } issue_186_beautify: { @@ -295,7 +295,7 @@ issue_186_beautify: { else bar(); } - expect_exact: 'var x = 3;\n\nif (foo()) do do alert(x); while (--x); while (x); else bar();' + expect_exact: 'var x = 3;\n\nif (foo()) do {\n do {\n alert(x);\n } while (--x);\n} while (x); else bar();' } issue_186_beautify_ie8: { @@ -314,7 +314,7 @@ issue_186_beautify_ie8: { else bar(); } - expect_exact: 'var x = 3;\n\nif (foo()) do do alert(x); while (--x) while (x) else bar();' + expect_exact: 'var x = 3;\n\nif (foo()) {\n do {\n do {\n alert(x);\n } while (--x);\n } while (x);\n} else bar();' } issue_186_bracketize: { @@ -394,5 +394,5 @@ issue_186_beautify_bracketize_ie8: { else bar(); } - expect_exact: 'var x = 3;\n\nif (foo()) {\n do {\n do {\n alert(x);\n } while (--x)\n } while (x)\n} else {\n bar();\n}' + expect_exact: 'var x = 3;\n\nif (foo()) {\n do {\n do {\n alert(x);\n } while (--x);\n } while (x);\n} else {\n bar();\n}' } From 3ee55748d466f36aff54383b53fb3deca144de97 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 8 Mar 2017 06:00:51 +0800 Subject: [PATCH 30/53] only run benchmark & jetstream on CI (#1571) --- .travis.yml | 5 +++- test/mocha/benchmark.js | 24 ------------------ test/mocha/release.js | 54 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 25 deletions(-) delete mode 100644 test/mocha/benchmark.js create mode 100644 test/mocha/release.js diff --git a/.travis.yml b/.travis.yml index b91a80e9..b2aef3dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,13 @@ language: node_js before_install: "npm install -g npm" node_js: - - "0.12" - "0.10" + - "0.12" - "4" - "6" + - "7" +env: + - UGLIFYJS_TEST_ALL=1 matrix: fast_finish: true sudo: false diff --git a/test/mocha/benchmark.js b/test/mocha/benchmark.js deleted file mode 100644 index c2c7c02c..00000000 --- a/test/mocha/benchmark.js +++ /dev/null @@ -1,24 +0,0 @@ -var assert = require("assert"); -var exec = require("child_process").exec; - -describe("test/benchmark.js", function() { - this.timeout(120000); - var command = '"' + process.argv[0] + '" test/benchmark.js '; - [ - "-b", - "-b bracketize", - "-m", - "-mc passes=3", - "-mc passes=3,toplevel", - "-mc passes=3,unsafe", - "-mc keep_fargs=false,passes=3", - "-mc keep_fargs=false,passes=3,pure_getters,unsafe,unsafe_comps,unsafe_math,unsafe_proto", - ].forEach(function(args) { - it("Should pass with options " + args, function(done) { - exec(command + args, function(err) { - if (err) throw err; - done(); - }); - }); - }); -}); diff --git a/test/mocha/release.js b/test/mocha/release.js new file mode 100644 index 00000000..3b2d9a72 --- /dev/null +++ b/test/mocha/release.js @@ -0,0 +1,54 @@ +var assert = require("assert"); +var spawn = require("child_process").spawn; + +if (!process.env.UGLIFYJS_TEST_ALL) return; + +function run(command, args, done) { + var id = setInterval(function() { + process.stdout.write("\0"); + }, 5 * 60 * 1000); + spawn(command, args, { + stdio: "ignore" + }).on("exit", function(code) { + clearInterval(id); + assert.strictEqual(code, 0); + done(); + }); +} + +describe("test/benchmark.js", function() { + this.timeout(5 * 60 * 1000); + [ + "-b", + "-b bracketize", + "-m", + "-mc passes=3", + "-mc passes=3,toplevel", + "-mc passes=3,unsafe", + "-mc keep_fargs=false,passes=3", + "-mc keep_fargs=false,passes=3,pure_getters,unsafe,unsafe_comps,unsafe_math,unsafe_proto", + ].forEach(function(options) { + it("Should pass with options " + options, function(done) { + var args = options.split(/ /); + args.unshift("test/benchmark.js"); + run(process.argv[0], args, done); + }); + }); +}); + +describe("test/jetstream.js", function() { + this.timeout(20 * 60 * 1000); + it("Should install phantomjs-prebuilt", function(done) { + run("npm", ["install", "phantomjs-prebuilt@2.1.14"], done); + }); + [ + "-mc warnings=false", + "-mc keep_fargs=false,passes=3,pure_getters,unsafe,unsafe_comps,unsafe_math,unsafe_proto,warnings=false", + ].forEach(function(options) { + it("Should pass with options " + options, function(done) { + var args = options.split(/ /); + args.unshift("test/jetstream.js"); + run(process.argv[0], args, done); + }); + }); +}); From c7cdcf06a65b70b557894c1680fc099d3c7aca6a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 8 Mar 2017 12:39:57 +0800 Subject: [PATCH 31/53] fix function name eliminiation (#1576) Function expression can be assigned to a variable and be given a name. Ensure function name is the reduced variable before clearing it out. fixes #1573 fixes #1575 --- lib/compress.js | 3 ++- test/compress/reduce_vars.js | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 85b457e3..f423fdd4 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2611,7 +2611,8 @@ merge(Compressor.prototype, { if (compressor.option("unused") && def.references.length == 1 && compressor.find_parent(AST_Scope) === def.scope) { - if (!compressor.option("keep_fnames")) { + if (!compressor.option("keep_fnames") + && exp.name && exp.name.definition() === def) { exp.name = null; } self.expression = exp; diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 53e28152..10dc9d98 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1122,3 +1122,25 @@ defun_label: { }(); } } + +double_reference: { + options = { + reduce_vars: true, + unused: true, + } + input: { + function f() { + var g = function g() { + g(); + }; + g(); + } + } + expect: { + function f() { + (function g() { + g(); + })(); + } + } +} From 344d11d591ca18416ce6fe7444e451609ee14689 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 8 Mar 2017 12:41:22 +0800 Subject: [PATCH 32/53] v2.8.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 086b6910..6030d798 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.8", + "version": "2.8.9", "engines": { "node": ">=0.8.0" }, From 711f88dcb49bc0daf0548f3ec240f680e05dfc27 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 8 Mar 2017 18:37:32 +0800 Subject: [PATCH 33/53] scan assignment value in drop_unused() (#1578) those were not optimised for `unused` before, which made it necessary for `reduce_vars` to have separate steps for `keep_fnames` docs update by @kzc closes #1577 --- README.md | 8 ++++---- lib/compress.js | 5 +---- test/compress/drop-unused.js | 25 +++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f880fd7b..a33e0b32 100644 --- a/README.md +++ b/README.md @@ -391,11 +391,11 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `cascade` -- small optimization for sequences, transform `x, x` into `x` and `x = something(), x` into `x = something()` -- `collapse_vars` -- default `false`. Collapse single-use `var` and `const` - definitions when possible. +- `collapse_vars` -- Collapse single-use `var` and `const` definitions + when possible. -- `reduce_vars` -- default `false`. Improve optimization on variables assigned - with and used as constant values. +- `reduce_vars` -- Improve optimization on variables assigned with and + used as constant values. - `warnings` -- display warnings when dropping unreachable code or unused declarations etc. diff --git a/lib/compress.js b/lib/compress.js index f423fdd4..302f8f56 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1840,6 +1840,7 @@ merge(Compressor.prototype, { } if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { var def = node.definitions.filter(function(def){ + if (def.value) def.value = def.value.transform(tt); if (def.name.definition().id in in_use_ids) return true; var w = { name : def.name.name, @@ -2611,10 +2612,6 @@ merge(Compressor.prototype, { if (compressor.option("unused") && def.references.length == 1 && compressor.find_parent(AST_Scope) === def.scope) { - if (!compressor.option("keep_fnames") - && exp.name && exp.name.definition() === def) { - exp.name = null; - } self.expression = exp; } } diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 20dab3b9..9c960561 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -700,3 +700,28 @@ issue_1539: { } } } + +vardef_value: { + options = { + keep_fnames: false, + reduce_vars: true, + unused: true, + } + input: { + function f() { + function g(){ + return x(); + } + var a = g(); + return a(42); + } + } + expect: { + function f() { + var a = function(){ + return x(); + }(); + return a(42); + } + } +} From 80e81765cf073be2f62010cc9d63bf6c3106a714 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 8 Mar 2017 18:56:01 +0800 Subject: [PATCH 34/53] explain how to make a proper bug report (#1579) fixes #1574 --- .github/ISSUE_TEMPLATE.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..6c62c47f --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,7 @@ +- Bug report or feature request? +- [ ] `uglify-js` version (`uglifyjs -V`) +- [ ] JavaScript input - ideally as small as possible. +- [ ] The `uglifyjs` CLI command executed or `minify()` options used. +- [ ] An example of JavaScript output produced and/or the error or warning. + +Note: the release version of `uglify-js` only supports ES5. Those wishing to minify ES6 should use the experimental [`harmony`](https://github.com/mishoo/UglifyJS2#harmony) branch. \ No newline at end of file From aa80ee349d7c534a950ce6cba00c59ca0f7b5629 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 8 Mar 2017 19:19:54 +0800 Subject: [PATCH 35/53] remove checkboxes from Issues template --- .github/ISSUE_TEMPLATE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6c62c47f..31ba69d3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,7 @@ - Bug report or feature request? -- [ ] `uglify-js` version (`uglifyjs -V`) -- [ ] JavaScript input - ideally as small as possible. -- [ ] The `uglifyjs` CLI command executed or `minify()` options used. -- [ ] An example of JavaScript output produced and/or the error or warning. +- `uglify-js` version (`uglifyjs -V`) +- JavaScript input - ideally as small as possible. +- The `uglifyjs` CLI command executed or `minify()` options used. +- An example of JavaScript output produced and/or the error or warning. -Note: the release version of `uglify-js` only supports ES5. Those wishing to minify ES6 should use the experimental [`harmony`](https://github.com/mishoo/UglifyJS2#harmony) branch. \ No newline at end of file +Note: the release version of `uglify-js` only supports ES5. Those wishing to minify ES6 should use the experimental [`harmony`](https://github.com/mishoo/UglifyJS2#harmony) branch. From 7e465d4a01f2cbcae24e1c60ee96969c14130287 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 9 Mar 2017 05:22:27 +0800 Subject: [PATCH 36/53] scan RHS of dropped assignments (#1581) - similar case as #1578 but against #1450 instead - fix `this` binding in #1450 as well closes #1580 --- lib/compress.js | 18 ++++++++---------- test/compress/drop-unused.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 302f8f56..f6b76ec2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1901,17 +1901,15 @@ merge(Compressor.prototype, { } return node; } - if (drop_vars && assign_as_unused) { - var n = node; - while (n instanceof AST_Assign - && n.operator == "=" - && n.left instanceof AST_SymbolRef) { - var def = n.left.definition(); - if (def.id in in_use_ids - || self.variables.get(def.name) !== def) break; - n = n.right; + if (drop_vars && assign_as_unused + && node instanceof AST_Assign + && node.operator == "=" + && node.left instanceof AST_SymbolRef) { + var def = node.left.definition(); + if (!(def.id in in_use_ids) + && self.variables.get(def.name) === def) { + return maintain_this_binding(tt.parent(), node, node.right.transform(tt)); } - if (n !== node) return n; } if (node instanceof AST_For) { descend(node, this); diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 9c960561..728557a6 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -725,3 +725,39 @@ vardef_value: { } } } + +assign_binding: { + options = { + cascade: true, + side_effects: true, + unused: true, + } + input: { + function f() { + var a; + a = f.g, a(); + } + } + expect: { + function f() { + (0, f.g)(); + } + } +} + +assign_chain: { + options = { + unused: true, + } + input: { + function f() { + var a, b; + x = a = y = b = 42; + } + } + expect: { + function f() { + x = y = 42; + } + } +} From e9920f7ca162ce062cc481b876be293d7324a714 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 9 Mar 2017 05:48:06 +0800 Subject: [PATCH 37/53] v2.8.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6030d798..c56d8f86 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.9", + "version": "2.8.10", "engines": { "node": ">=0.8.0" }, From b633706ce42576b7e2aa85a96c5691bde87e71ac Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 9 Mar 2017 19:11:05 +0800 Subject: [PATCH 38/53] fix & improve function argument compression (#1584) - one-use function call => IIFE should take `eval()` & `arguments` into account - if unused parameter cannot be eliminated, replace it with `0` fixes #1583 --- lib/compress.js | 46 ++++++++++----- test/compress/drop-unused.js | 30 ++++++++++ test/compress/reduce_vars.js | 108 +++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 13 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f6b76ec2..3964636f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -281,6 +281,9 @@ merge(Compressor.prototype, { if (node instanceof AST_Function && (iife = tw.parent()) instanceof AST_Call && iife.expression === node) { + // Virtually turn IIFE parameters into variable definitions: + // (function(a,b) {...})(c,d) => (function() {var a=c,b=d; ...})() + // So existing transformation rules can work on them. node.argnames.forEach(function(arg, i) { var d = arg.definition(); d.fixed = iife.args[i] || make_node(AST_Undefined, iife); @@ -1810,10 +1813,12 @@ merge(Compressor.prototype, { node.name = null; } if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { - if (!compressor.option("keep_fargs")) { - for (var a = node.argnames, i = a.length; --i >= 0;) { - var sym = a[i]; - if (!(sym.definition().id in in_use_ids)) { + var trim = !compressor.option("keep_fargs"); + for (var a = node.argnames, i = a.length; --i >= 0;) { + var sym = a[i]; + if (!(sym.definition().id in in_use_ids)) { + sym.__unused = true; + if (trim) { a.pop(); compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { name : sym.name, @@ -1822,7 +1827,9 @@ merge(Compressor.prototype, { col : sym.start.col }); } - else break; + } + else { + trim = false; } } } @@ -2609,6 +2616,9 @@ merge(Compressor.prototype, { exp = def.fixed; if (compressor.option("unused") && def.references.length == 1 + && !(def.scope.uses_arguments + && def.orig[0] instanceof AST_SymbolFunarg) + && !def.scope.uses_eval && compressor.find_parent(AST_Scope) === def.scope) { self.expression = exp; } @@ -2617,16 +2627,26 @@ merge(Compressor.prototype, { if (compressor.option("unused") && exp instanceof AST_Function && !exp.uses_arguments - && !exp.uses_eval - && self.args.length > exp.argnames.length) { - var end = exp.argnames.length; - for (var i = end, len = self.args.length; i < len; i++) { - var node = self.args[i].drop_side_effect_free(compressor); - if (node) { - self.args[end++] = node; + && !exp.uses_eval) { + var pos = 0, last = 0; + for (var i = 0, len = self.args.length; i < len; i++) { + var trim = i >= exp.argnames.length; + if (trim || exp.argnames[i].__unused) { + var node = self.args[i].drop_side_effect_free(compressor); + if (node) { + self.args[pos++] = node; + } else if (!trim) { + self.args[pos++] = make_node(AST_Number, self.args[i], { + value: 0 + }); + continue; + } + } else { + self.args[pos++] = self.args[i]; } + last = pos; } - self.args.length = end; + self.args.length = last; } if (compressor.option("unsafe")) { if (exp instanceof AST_SymbolRef && exp.undeclared()) { diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 728557a6..9f3bf77d 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -761,3 +761,33 @@ assign_chain: { } } } + +issue_1583: { + options = { + keep_fargs: true, + reduce_vars: true, + unused: true, + } + input: { + function m(t) { + (function(e) { + t = e(); + })(function() { + return (function(a) { + return a; + })(function(a) {}); + }); + } + } + expect: { + function m(t) { + (function(e) { + t = (function() { + return (function(a) { + return a; + })(function(a) {}); + })(); + })(); + } + } +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 10dc9d98..734ce4ed 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1144,3 +1144,111 @@ double_reference: { } } } + +iife_arguments_1: { + options = { + reduce_vars: true, + unused: true, + } + input: { + (function(x) { + console.log(x() === arguments[0]); + })(function f() { + return f; + }); + } + expect: { + (function(x) { + console.log(x() === arguments[0]); + })(function f() { + return f; + }); + } +} + +iife_arguments_2: { + options = { + reduce_vars: true, + unused: true, + } + input: { + (function() { + var x = function f() { + return f; + }; + console.log(x() === arguments[0]); + })(); + } + expect: { + (function() { + console.log(function f() { + return f; + }() === arguments[0]); + })(); + } +} + +iife_eval_1: { + options = { + reduce_vars: true, + unused: true, + } + input: { + (function(x) { + console.log(x() === eval("x")); + })(function f() { + return f; + }); + } + expect: { + (function(x) { + console.log(x() === eval("x")); + })(function f() { + return f; + }); + } +} + +iife_eval_2: { + options = { + reduce_vars: true, + unused: true, + } + input: { + (function() { + var x = function f() { + return f; + }; + console.log(x() === eval("x")); + })(); + } + expect: { + (function() { + var x = function f() { + return f; + }; + console.log(x() === eval("x")); + })(); + } +} + +iife_func_side_effects: { + options = { + reduce_vars: true, + unused: true, + } + input: { + (function(a, b, c) { + return b(); + })(x(), function() { + return y(); + }, z()); + } + expect: { + (function(a, b, c) { + return function() { + return y(); + }(); + })(x(), 0, z()); + } +} From 93cdb194f4895384b68a66736366659c39caece8 Mon Sep 17 00:00:00 2001 From: Michael Mior Date: Thu, 9 Mar 2017 14:08:43 -0500 Subject: [PATCH 39/53] Correctly raise a parse exception with a missing loop body (#1585) --- lib/parse.js | 2 ++ test/input/invalid/loop-no-body.js | 1 + test/mocha/cli.js | 13 +++++++++++++ 3 files changed, 16 insertions(+) create mode 100644 test/input/invalid/loop-no-body.js diff --git a/lib/parse.js b/lib/parse.js index 9aadc9f5..5e140862 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -787,6 +787,8 @@ function parse($TEXT, options) { return function() { var start = S.token; var expr = parser(); + if (!expr) croak("Expected expression"); + var end = prev(); expr.start = start; expr.end = end; diff --git a/test/input/invalid/loop-no-body.js b/test/input/invalid/loop-no-body.js new file mode 100644 index 00000000..07b27428 --- /dev/null +++ b/test/input/invalid/loop-no-body.js @@ -0,0 +1 @@ +for (var i = 0; i < 1; i++) diff --git a/test/mocha/cli.js b/test/mocha/cli.js index e8e07cb5..0b4fe007 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -238,4 +238,17 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should fail with a missing loop body", function(done) { + var command = uglifyjscmd + ' test/input/invalid/loop-no-body.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + var lines = stderr.split(/\n/); + assert.strictEqual(lines[0], "Parse error at test/input/invalid/loop-no-body.js:2,0"); + assert.strictEqual(lines[1], "for (var i = 0; i < 1; i++) "); + assert.strictEqual(lines[2], " ^"); + assert.strictEqual(lines[3], "SyntaxError: Expected expression"); + done(); + }); + }); }); From 9e6b128374c62ee9f6238134fdc207ec9dc86284 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 10 Mar 2017 03:15:21 +0800 Subject: [PATCH 40/53] fix catch variable reference in IE8 (#1587) `AST_Scope.def_variable()` will overwrite `AST_Symbol.thedef`, so save a copy before calling. fixes #1586 --- lib/scope.js | 3 ++- test/compress/screw-ie8.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/scope.js b/lib/scope.js index f23c8fe2..483503ee 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -212,9 +212,10 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ self.walk(new TreeWalker(function(node, descend) { if (node instanceof AST_SymbolCatch) { var name = node.name; + var refs = node.thedef.references; var scope = node.thedef.scope.parent_scope; var def = scope.find_variable(name) || self.globals.get(name) || scope.def_variable(node); - node.thedef.references.forEach(function(ref) { + refs.forEach(function(ref) { ref.thedef = def; ref.reference(options); }); diff --git a/test/compress/screw-ie8.js b/test/compress/screw-ie8.js index 36eb4d3a..4fbb95c8 100644 --- a/test/compress/screw-ie8.js +++ b/test/compress/screw-ie8.js @@ -182,3 +182,39 @@ reduce_vars: { } } } + +issue_1586_1: { + options = { + screw_ie8: false, + } + mangle = { + screw_ie8: false, + } + input: { + function f() { + try { + } catch (err) { + console.log(err.message); + } + } + } + expect_exact: "function f(){try{}catch(c){console.log(c.message)}}" +} + +issue_1586_2: { + options = { + screw_ie8: true, + } + mangle = { + screw_ie8: true, + } + input: { + function f() { + try { + } catch (err) { + console.log(err.message); + } + } + } + expect_exact: "function f(){try{}catch(c){console.log(c.message)}}" +} From 8354758f3005e98de7ef2b50a01e903d79e4d265 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 10 Mar 2017 04:17:21 +0800 Subject: [PATCH 41/53] v2.8.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c56d8f86..0a5d814d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.10", + "version": "2.8.11", "engines": { "node": ">=0.8.0" }, From cf45e2f79b543ebae60c5de54166b20da4522c25 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 10 Mar 2017 10:49:41 +0800 Subject: [PATCH 42/53] fixup for #1585 (#1589) As patched on `harmony`, `statement()` is the only user of `embed_tokens()` with a missing error branch. Updated test case and match up with `harmony` to facilitate future merging. --- lib/parse.js | 6 +----- test/mocha/cli.js | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 5e140862..25fe9319 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -787,8 +787,6 @@ function parse($TEXT, options) { return function() { var start = S.token; var expr = parser(); - if (!expr) croak("Expected expression"); - var end = prev(); expr.start = start; expr.end = end; @@ -930,11 +928,9 @@ function parse($TEXT, options) { expression : parenthesised(), body : statement() }); - - default: - unexpected(); } } + unexpected(); }); function labeled_statement() { diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 0b4fe007..fa952d91 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -247,7 +247,7 @@ describe("bin/uglifyjs", function () { assert.strictEqual(lines[0], "Parse error at test/input/invalid/loop-no-body.js:2,0"); assert.strictEqual(lines[1], "for (var i = 0; i < 1; i++) "); assert.strictEqual(lines[2], " ^"); - assert.strictEqual(lines[3], "SyntaxError: Expected expression"); + assert.strictEqual(lines[3], "SyntaxError: Unexpected token: eof (undefined)"); done(); }); }); From be80f7e706cd6eb1c5f06e433804fda589a8968a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 10 Mar 2017 11:27:30 +0800 Subject: [PATCH 43/53] support multi-line string in tests (#1590) `expect_exact` sometimes have multiple lines and `\n` are hard to read. Use array of strings to emulate line breaks and improve readability. --- test/compress/loops.js | 50 ++++++++++++++++++++++++++++++++--- test/compress/max_line_len.js | 15 +++++++++-- test/run-tests.js | 24 ++++++++++++----- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/test/compress/loops.js b/test/compress/loops.js index df3011cd..7581e758 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -295,7 +295,15 @@ issue_186_beautify: { else bar(); } - expect_exact: 'var x = 3;\n\nif (foo()) do {\n do {\n alert(x);\n } while (--x);\n} while (x); else bar();' + expect_exact: [ + 'var x = 3;', + '', + 'if (foo()) do {', + ' do {', + ' alert(x);', + ' } while (--x);', + '} while (x); else bar();', + ] } issue_186_beautify_ie8: { @@ -314,7 +322,17 @@ issue_186_beautify_ie8: { else bar(); } - expect_exact: 'var x = 3;\n\nif (foo()) {\n do {\n do {\n alert(x);\n } while (--x);\n } while (x);\n} else bar();' + expect_exact: [ + 'var x = 3;', + '', + 'if (foo()) {', + ' do {', + ' do {', + ' alert(x);', + ' } while (--x);', + ' } while (x);', + '} else bar();', + ] } issue_186_bracketize: { @@ -374,7 +392,19 @@ issue_186_beautify_bracketize: { else bar(); } - expect_exact: 'var x = 3;\n\nif (foo()) {\n do {\n do {\n alert(x);\n } while (--x);\n } while (x);\n} else {\n bar();\n}' + expect_exact: [ + 'var x = 3;', + '', + 'if (foo()) {', + ' do {', + ' do {', + ' alert(x);', + ' } while (--x);', + ' } while (x);', + '} else {', + ' bar();', + '}', + ] } issue_186_beautify_bracketize_ie8: { @@ -394,5 +424,17 @@ issue_186_beautify_bracketize_ie8: { else bar(); } - expect_exact: 'var x = 3;\n\nif (foo()) {\n do {\n do {\n alert(x);\n } while (--x);\n } while (x);\n} else {\n bar();\n}' + expect_exact: [ + 'var x = 3;', + '', + 'if (foo()) {', + ' do {', + ' do {', + ' alert(x);', + ' } while (--x);', + ' } while (x);', + '} else {', + ' bar();', + '}', + ] } diff --git a/test/compress/max_line_len.js b/test/compress/max_line_len.js index b9e09178..7ad9ee0e 100644 --- a/test/compress/max_line_len.js +++ b/test/compress/max_line_len.js @@ -7,7 +7,13 @@ too_short: { return { c: 42, d: a(), e: "foo"}; } } - expect_exact: 'function f(a){\nreturn{\nc:42,\nd:a(),\ne:"foo"}}' + expect_exact: [ + 'function f(a){', + 'return{', + 'c:42,', + 'd:a(),', + 'e:"foo"}}', + ] expect_warnings: [ "WARN: Output exceeds 10 characters" ] @@ -22,7 +28,12 @@ just_enough: { return { c: 42, d: a(), e: "foo"}; } } - expect_exact: 'function f(a){\nreturn{c:42,\nd:a(),e:"foo"}\n}' + expect_exact: [ + 'function f(a){', + 'return{c:42,', + 'd:a(),e:"foo"}', + '}', + ] expect_warnings: [ ] } diff --git a/test/run-tests.js b/test/run-tests.js index 15a12c6b..36d26ef7 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -214,6 +214,23 @@ function parse_test(file) { })); } + function read_string(stat) { + if (stat.TYPE === "SimpleStatement") { + var body = stat.body; + out: switch(body.TYPE) { + case "String": + return body.value; + case "Array": + return body.elements.map(function(element) { + if (element.TYPE !== "String") + throw new Error("Should be array of strings"); + return element.value; + }).join("\n"); + } + } + throw new Error("Should be string or array of strings"); + } + function get_one_test(name, block) { var test = { name: name, options: {} }; var tw = new U.TreeWalker(function(node, descend){ @@ -240,12 +257,7 @@ function parse_test(file) { else if (stat.body.length == 0) stat = new U.AST_EmptyStatement(); } if (node.label.name === "expect_exact") { - if (!(stat.TYPE === "SimpleStatement" && stat.body.TYPE === "String")) { - throw new Error( - "The value of the expect_exact clause should be a string, " + - "like `expect_exact: \"some.exact.javascript;\"`"); - } - test[node.label.name] = stat.body.start.value + test[node.label.name] = read_string(stat); } else { test[node.label.name] = stat; } From d9344f30b83ecdfc8310ff43b9361c67cc85ec3e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 11 Mar 2017 03:34:55 +0800 Subject: [PATCH 44/53] disallow parameter substitution for named IIFEs (#1596) Self-referenced function has non-fixed values assigned to its parameters. Let `unused` & `!keep_fnames` do the scanning, then apply `reduce_vars` only to unnamed functions. fixes #1595 --- lib/compress.js | 1 + test/compress/reduce_vars.js | 75 ++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 3964636f..7302f5b9 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -279,6 +279,7 @@ merge(Compressor.prototype, { } var iife; if (node instanceof AST_Function + && !node.name && (iife = tw.parent()) instanceof AST_Call && iife.expression === node) { // Virtually turn IIFE parameters into variable definitions: diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 734ce4ed..a5ab59f9 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1252,3 +1252,78 @@ iife_func_side_effects: { })(x(), 0, z()); } } + +issue_1595_1: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + (function f(a) { + return f(a + 1); + })(2); + } + expect: { + (function f(a) { + return f(a + 1); + })(2); + } +} + +issue_1595_2: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + (function f(a) { + return g(a + 1); + })(2); + } + expect: { + (function(a) { + return g(a + 1); + })(2); + } +} + +issue_1595_3: { + options = { + evaluate: true, + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + (function f(a) { + return g(a + 1); + })(2); + } + expect: { + (function(a) { + return g(3); + })(); + } +} + +issue_1595_4: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + (function iife(a, b, c) { + console.log(a, b, c); + if (a) iife(a - 1, b, c); + })(3, 4, 5); + } + expect: { + (function iife(a, b, c) { + console.log(a, b, c); + if (a) iife(a - 1, b, c); + })(3, 4, 5); + } +} From e3a3db73ae4c2c90ad304e1e6b7d019053ca3dc3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 11 Mar 2017 04:59:55 +0800 Subject: [PATCH 45/53] temporary fix for boolean bug (#1597) fixes #1592 --- lib/compress.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 7302f5b9..e3ae5bde 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -404,7 +404,7 @@ merge(Compressor.prototype, { return make_node(AST_Number, orig, { value: val }); case "boolean": - return make_node(val ? AST_True : AST_False, orig).transform(compressor); + return make_node(val ? AST_True : AST_False, orig).optimize(compressor); case "undefined": return make_node(AST_Undefined, orig).transform(compressor); default: From 919d5e348249f7b8c5ef0581660817292b5308a6 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 11 Mar 2017 05:00:55 +0800 Subject: [PATCH 46/53] v2.8.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a5d814d..2ba24c7c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.11", + "version": "2.8.12", "engines": { "node": ">=0.8.0" }, From 381bd3836ecd79eb5d4f7b84807c778ee1acf9c9 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 14 Mar 2017 13:19:05 +0800 Subject: [PATCH 47/53] minor clean-ups (#1600) - remove obsolete optimisation in `AST_Binary` after #1477 - improve `TreeWalker.has_directive()` readability and resilience against multiple visits --- lib/ast.js | 6 +++--- lib/compress.js | 12 +----------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index f7ab52e2..092a9590 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -984,8 +984,8 @@ TreeWalker.prototype = { push: function (node) { if (node instanceof AST_Lambda) { this.directives = Object.create(this.directives); - } else if (node instanceof AST_Directive) { - this.directives[node.value] = this.directives[node.value] ? "up" : true; + } else if (node instanceof AST_Directive && !this.directives[node.value]) { + this.directives[node.value] = node; } this.stack.push(node); }, @@ -1013,7 +1013,7 @@ TreeWalker.prototype = { for (var i = 0; i < node.body.length; ++i) { var st = node.body[i]; if (!(st instanceof AST_Directive)) break; - if (st.value == type) return true; + if (st.value == type) return st; } } }, diff --git a/lib/compress.js b/lib/compress.js index e3ae5bde..ab4c3c2f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1664,7 +1664,7 @@ merge(Compressor.prototype, { /* -----[ optimizers ]----- */ OPT(AST_Directive, function(self, compressor){ - if (compressor.has_directive(self.value) === "up") { + if (compressor.has_directive(self.value) !== self) { return make_node(AST_EmptyStatement, self); } return self; @@ -3018,16 +3018,6 @@ merge(Compressor.prototype, { var commutativeOperators = makePredicate("== === != !== * & | ^"); OPT(AST_Binary, function(self, compressor){ - var lhs = self.left.evaluate(compressor); - var rhs = self.right.evaluate(compressor); - if (lhs.length > 1 && lhs[0].is_constant() !== self.left.is_constant() - || rhs.length > 1 && rhs[0].is_constant() !== self.right.is_constant()) { - return make_node(AST_Binary, self, { - operator: self.operator, - left: lhs[0], - right: rhs[0] - }).optimize(compressor); - } function reversible() { return self.left instanceof AST_Constant || self.right instanceof AST_Constant From 8223b2e0db4cc41d467d9b94b05511a36c320184 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 15 Mar 2017 18:44:13 +0800 Subject: [PATCH 48/53] fix `AST_Node.optimize()` (#1602) Liberal use of `Compressor.transform()` and `AST_Node.optimize()` presents an issue for look-up operations like `TreeWalker.in_boolean_context()` and `TreeWalker.parent()`. This is an incremental fix such that `AST_Node.optimize()` would now contain the correct stack information when called correctly. --- lib/compress.js | 69 +++++++++----------- lib/utils.js | 4 +- test/compress/transform.js | 129 +++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 38 deletions(-) create mode 100644 test/compress/transform.js diff --git a/lib/compress.js b/lib/compress.js index ab4c3c2f..59a96684 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -152,13 +152,14 @@ merge(Compressor.prototype, { was_scope = true; } descend(node, this); - node = node.optimize(this); - if (was_scope && node instanceof AST_Scope) { - node.drop_unused(this); - descend(node, this); + descend(node, this); + var opt = node.optimize(this); + if (was_scope && opt instanceof AST_Scope) { + opt.drop_unused(this); + descend(opt, this); } - node._squeezed = true; - return node; + if (opt === node) opt._squeezed = true; + return opt; } }); @@ -171,8 +172,7 @@ merge(Compressor.prototype, { if (compressor.has_directive("use asm")) return self; var opt = optimizer(self, compressor); opt._optimized = true; - if (opt === self) return opt; - return opt.transform(compressor); + return opt; }); }; @@ -914,7 +914,7 @@ merge(Compressor.prototype, { if (stat instanceof AST_LoopControl) { var lct = compressor.loopcontrol_target(stat.label); if ((stat instanceof AST_Break - && lct instanceof AST_BlockStatement + && !(lct instanceof AST_IterationStatement) && loop_body(lct) === self) || (stat instanceof AST_Continue && loop_body(lct) === self)) { if (stat.label) { @@ -1646,8 +1646,8 @@ merge(Compressor.prototype, { return thing && thing.aborts(); }; (function(def){ - def(AST_Statement, function(){ return null }); - def(AST_Jump, function(){ return this }); + def(AST_Statement, return_null); + def(AST_Jump, return_this); function block_aborts(){ var n = this.body.length; return n > 0 && aborts(this.body[n - 1]); @@ -2077,14 +2077,6 @@ merge(Compressor.prototype, { // drop_side_effect_free() // remove side-effect-free parts which only affects return value (function(def){ - function return_this() { - return this; - } - - function return_null() { - return null; - } - // Drop side-effect-free elements from an array of expressions. // Returns an array of expressions with side-effects or null // if all elements were dropped. Note: original array may be @@ -2358,7 +2350,7 @@ merge(Compressor.prototype, { extract_declarations_from_unreachable_code(compressor, self.alternative, a); } a.push(self.body); - return make_node(AST_BlockStatement, self, { body: a }).transform(compressor); + return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } } else { compressor.warn("Condition always false [{file}:{line},{col}]", self.condition.start); @@ -2366,7 +2358,7 @@ merge(Compressor.prototype, { var a = []; extract_declarations_from_unreachable_code(compressor, self.body, a); if (self.alternative) a.push(self.alternative); - return make_node(AST_BlockStatement, self, { body: a }).transform(compressor); + return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } } } @@ -2385,8 +2377,8 @@ merge(Compressor.prototype, { } if (is_empty(self.body) && is_empty(self.alternative)) { return make_node(AST_SimpleStatement, self.condition, { - body: self.condition - }).transform(compressor); + body: self.condition.clone() + }).optimize(compressor); } if (self.body instanceof AST_SimpleStatement && self.alternative instanceof AST_SimpleStatement) { @@ -2396,7 +2388,7 @@ merge(Compressor.prototype, { consequent : statement_to_expression(self.body), alternative : statement_to_expression(self.alternative) }) - }).transform(compressor); + }).optimize(compressor); } if (is_empty(self.alternative) && self.body instanceof AST_SimpleStatement) { if (self_condition_length === negated_length && !negated_is_best @@ -2412,14 +2404,14 @@ merge(Compressor.prototype, { left : negated, right : statement_to_expression(self.body) }) - }).transform(compressor); + }).optimize(compressor); return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "&&", left : self.condition, right : statement_to_expression(self.body) }) - }).transform(compressor); + }).optimize(compressor); } if (self.body instanceof AST_EmptyStatement && self.alternative @@ -2430,7 +2422,7 @@ merge(Compressor.prototype, { left : self.condition, right : statement_to_expression(self.alternative) }) - }).transform(compressor); + }).optimize(compressor); } if (self.body instanceof AST_Exit && self.alternative instanceof AST_Exit @@ -2440,18 +2432,21 @@ merge(Compressor.prototype, { condition : self.condition, consequent : self.body.value || make_node(AST_Undefined, self.body), alternative : self.alternative.value || make_node(AST_Undefined, self.alternative) - }) - }).transform(compressor); + }).transform(compressor) + }).optimize(compressor); } if (self.body instanceof AST_If && !self.body.alternative && !self.alternative) { - self.condition = make_node(AST_Binary, self.condition, { - operator: "&&", - left: self.condition, - right: self.body.condition - }).transform(compressor); - self.body = self.body.body; + self = make_node(AST_If, self, { + condition: make_node(AST_Binary, self.condition, { + operator: "&&", + left: self.condition, + right: self.body.condition + }), + body: self.body.body, + alternative: null + }); } if (aborts(self.body)) { if (self.alternative) { @@ -2459,7 +2454,7 @@ merge(Compressor.prototype, { self.alternative = null; return make_node(AST_BlockStatement, self, { body: [ self, alt ] - }).transform(compressor); + }).optimize(compressor); } } if (aborts(self.alternative)) { @@ -2469,7 +2464,7 @@ merge(Compressor.prototype, { self.alternative = null; return make_node(AST_BlockStatement, self, { body: [ self, body ] - }).transform(compressor); + }).optimize(compressor); } return self; }); diff --git a/lib/utils.js b/lib/utils.js index da663546..fdb20471 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -126,9 +126,11 @@ function merge(obj, ext) { return count; }; -function noop() {}; +function noop() {} function return_false() { return false; } function return_true() { return true; } +function return_this() { return this; } +function return_null() { return null; } var MAP = (function(){ function MAP(a, f, backwards) { diff --git a/test/compress/transform.js b/test/compress/transform.js new file mode 100644 index 00000000..7b616e40 --- /dev/null +++ b/test/compress/transform.js @@ -0,0 +1,129 @@ +booleans_evaluate: { + options = { + booleans: true, + evaluate: true, + } + input: { + console.log(typeof void 0 != "undefined"); + console.log(1 == 1, 1 === 1) + console.log(1 != 1, 1 !== 1) + } + expect: { + console.log(!1); + console.log(!0, !0); + console.log(!1, !1); + } +} + +booleans_global_defs: { + options = { + booleans: true, + evaluate: true, + global_defs: { + A: true, + }, + } + input: { + console.log(A == 1); + } + expect: { + console.log(!0); + } +} + +condition_evaluate: { + options = { + booleans: true, + dead_code: false, + evaluate: true, + loops: false, + } + input: { + while (1 === 2); + for (; 1 == true;); + if (void 0 == null); + } + expect: { + while (!1); + for (; !0;); + if (!0); + } +} + +if_else_empty: { + options = { + conditionals: true, + } + input: { + if ({} ? a : b); else {} + } + expect: { + !{} ? b : a; + } +} + +label_if_break: { + options = { + conditionals: true, + dead_code: true, + evaluate: true, + } + input: { + L: if (true) { + a; + break L; + } + } + expect: { + a; + } +} + +while_if_break: { + options = { + conditionals: true, + loops: true, + sequences: true, + } + input: { + while (a) { + if (b) if(c) d; + if (e) break; + } + } + expect: { + for(; a && (b && c && d, !e);); + } +} + +if_return: { + options = { + booleans: true, + conditionals: true, + if_return: true, + sequences: true, + } + input: { + function f(w, x, y, z) { + if (x) return; + if (w) { + if (y) return; + } else if (z) return; + if (x == y) return true; + + if (x) w(); + if (y) z(); + return true; + } + } + expect: { + function f(w, x, y, z) { + if (!x) { + if (w) { + if (y) return; + } else if (z) return; + return x == y || (x && w(), y && z(), !0); + } + } + } +} From cf4bf4ceb1eee86197d51e77e640e59ca04739b8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 16 Mar 2017 01:02:59 +0800 Subject: [PATCH 49/53] fix stack issues with `AST_Node.evaluate()` (#1603) As patched in #1597, `make_node_from_constant()` makes inconsistent and sometimes incorrect calls to `optimize()` and `transform()`. Fix those issues properly by changing the semantics of `evaluate()` and `make_node_from_constant()`, with the side effect that `evaluate()` no longer eagerly converts constant to `AST_Node`. --- lib/compress.js | 261 ++++++++++++++++++++++++++---------------------- 1 file changed, 141 insertions(+), 120 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 59a96684..b3004fb5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -98,10 +98,10 @@ function Compressor(options, false_by_default) { this.top_retain = function(def) { return top_retain.test(def.name); }; - } else if (typeof top_retain === "function") { + } else if (typeof top_retain == "function") { this.top_retain = top_retain; } else if (top_retain) { - if (typeof top_retain === "string") { + if (typeof top_retain == "string") { top_retain = top_retain.split(/,/); } this.top_retain = function(def) { @@ -151,7 +151,17 @@ merge(Compressor.prototype, { node = node.hoist_declarations(this); was_scope = true; } + // Before https://github.com/mishoo/UglifyJS2/pull/1602 AST_Node.optimize() + // would call AST_Node.transform() if a different instance of AST_Node is + // produced after OPT(). + // This corrupts TreeWalker.stack, which cause AST look-ups to malfunction. + // Migrate and defer all children's AST_Node.transform() to below, which + // will now happen after this parent AST_Node has been properly substituted + // thus gives a consistent AST snapshot. descend(node, this); + // Existing code relies on how AST_Node.optimize() worked, and omitting the + // following replacement call would result in degraded efficiency of both + // output and performance. descend(node, this); var opt = node.optimize(this); if (was_scope && opt instanceof AST_Scope) { @@ -384,7 +394,7 @@ merge(Compressor.prototype, { return new ctor(props); }; - function make_node_from_constant(compressor, val, orig) { + function make_node_from_constant(val, orig) { switch (typeof val) { case "string": return make_node(AST_String, orig, { @@ -404,9 +414,9 @@ merge(Compressor.prototype, { return make_node(AST_Number, orig, { value: val }); case "boolean": - return make_node(val ? AST_True : AST_False, orig).optimize(compressor); + return make_node(val ? AST_True : AST_False, orig); case "undefined": - return make_node(AST_Undefined, orig).transform(compressor); + return make_node(AST_Undefined, orig); default: if (val === null) { return make_node(AST_Null, orig, { value: null }); @@ -1198,11 +1208,11 @@ merge(Compressor.prototype, { } } }); - function to_node(compressor, value, orig) { + function to_node(value, orig) { if (value instanceof AST_Node) return make_node(value.CTOR, orig, value); if (Array.isArray(value)) return make_node(AST_Array, orig, { elements: value.map(function(value) { - return to_node(compressor, value, orig); + return to_node(value, orig); }) }); if (value && typeof value == "object") { @@ -1210,14 +1220,14 @@ merge(Compressor.prototype, { for (var key in value) { props.push(make_node(AST_ObjectKeyVal, orig, { key: key, - value: to_node(compressor, value[key], orig) + value: to_node(value[key], orig) })); } return make_node(AST_Object, orig, { properties: props }); } - return make_node_from_constant(compressor, value, orig); + return make_node_from_constant(value, orig); } def(AST_Node, noop); def(AST_Dot, function(compressor, suffix){ @@ -1228,7 +1238,7 @@ merge(Compressor.prototype, { var name; var defines = compressor.option("global_defs"); if (defines && HOP(defines, (name = this.name + suffix))) { - var node = to_node(compressor, defines[name], this); + var node = to_node(defines[name], this); var top = compressor.find_parent(AST_Toplevel); node.walk(new TreeWalker(function(node) { if (node instanceof AST_SymbolRef) { @@ -1243,45 +1253,40 @@ merge(Compressor.prototype, { node.DEFMETHOD("_find_defs", func); }); - function best_of(ast1, ast2) { + function best_of_expression(ast1, ast2) { return ast1.print_to_string().length > ast2.print_to_string().length ? ast2 : ast1; } function best_of_statement(ast1, ast2) { - return best_of(make_node(AST_SimpleStatement, ast1, { + return best_of_expression(make_node(AST_SimpleStatement, ast1, { body: ast1 }), make_node(AST_SimpleStatement, ast2, { body: ast2 })).body; } + function best_of(compressor, ast1, ast2) { + return (first_in_statement(compressor) ? best_of_statement : best_of_expression)(ast1, ast2); + } + // methods to evaluate a constant expression (function (def){ - // The evaluate method returns an array with one or two - // elements. If the node has been successfully reduced to a - // constant, then the second element tells us the value; - // otherwise the second element is missing. The first element - // of the array is always an AST_Node descendant; if - // evaluation was successful it's a node that represents the - // constant; otherwise it's the original or a replacement node. + // If the node has been successfully reduced to a constant, + // then its value is returned; otherwise the element itself + // is returned. + // They can be distinguished as constant value is never a + // descendant of AST_Node. AST_Node.DEFMETHOD("evaluate", function(compressor){ - if (!compressor.option("evaluate")) return [ this ]; - var val; + if (!compressor.option("evaluate")) return this; try { - val = this._eval(compressor); + var val = this._eval(compressor); + return !val || val instanceof RegExp || typeof val != "object" ? val : this; } catch(ex) { if (ex !== def) throw ex; - return [ this ]; + return this; } - var node; - try { - node = make_node_from_constant(compressor, val, this); - } catch(ex) { - return [ this ]; - } - return [ best_of(node, this), val ]; }); var unaryPrefix = makePredicate("! ~ - +"); AST_Node.DEFMETHOD("is_constant", function(){ @@ -1319,8 +1324,8 @@ merge(Compressor.prototype, { })); } var result = this.evaluate(compressor); - if (result.length > 1) { - return result[1]; + if (result !== this) { + return result; } throw new Error(string_template("Cannot evaluate constant [{file}:{line},{col}]", this.start)); }); @@ -1480,9 +1485,9 @@ merge(Compressor.prototype, { var stat = make_node(AST_SimpleStatement, alt, { body: alt }); - return best_of(negated, stat) === stat ? alt : negated; + return best_of_expression(negated, stat) === stat ? alt : negated; } - return best_of(negated, alt); + return best_of_expression(negated, alt); } def(AST_Node, function(){ return basic_negation(this); @@ -2233,27 +2238,24 @@ merge(Compressor.prototype, { }); OPT(AST_DWLoop, function(self, compressor){ - var cond = self.condition.evaluate(compressor); - self.condition = cond[0]; if (!compressor.option("loops")) return self; - if (cond.length > 1) { - if (cond[1]) { + var cond = self.condition.evaluate(compressor); + if (cond !== self.condition) { + if (cond) { return make_node(AST_For, self, { body: self.body }); - } else if (self instanceof AST_While) { - if (compressor.option("dead_code")) { - var a = []; - extract_declarations_from_unreachable_code(compressor, self.body, a); - return make_node(AST_BlockStatement, self, { body: a }); - } + } else if (compressor.option("dead_code") && self instanceof AST_While) { + var a = []; + extract_declarations_from_unreachable_code(compressor, self.body, a); + return make_node(AST_BlockStatement, self, { body: a }); } else { - // self instanceof AST_Do - return self; + cond = make_node_from_constant(cond, self.condition).transform(compressor); + self.condition = best_of_expression(cond, self.condition); } } if (self instanceof AST_While) { - return make_node(AST_For, self, self).transform(compressor); + return make_node(AST_For, self, self).optimize(compressor); } return self; }); @@ -2275,7 +2277,7 @@ merge(Compressor.prototype, { var first = self.body instanceof AST_BlockStatement ? self.body.body[0] : self.body; if (first instanceof AST_If) { if (first.body instanceof AST_Break - && compressor.loopcontrol_target(first.body.label) === self) { + && compressor.loopcontrol_target(first.body.label) === compressor.self()) { if (self.condition) { self.condition = make_node(AST_Binary, self.condition, { left: self.condition, @@ -2288,7 +2290,7 @@ merge(Compressor.prototype, { drop_it(first.alternative); } else if (first.alternative instanceof AST_Break - && compressor.loopcontrol_target(first.alternative.label) === self) { + && compressor.loopcontrol_target(first.alternative.label) === compressor.self()) { if (self.condition) { self.condition = make_node(AST_Binary, self.condition, { left: self.condition, @@ -2304,27 +2306,25 @@ merge(Compressor.prototype, { }; OPT(AST_For, function(self, compressor){ - var cond = self.condition; - if (cond) { - cond = cond.evaluate(compressor); - self.condition = cond[0]; - } if (!compressor.option("loops")) return self; - if (cond) { - if (cond.length > 1 && !cond[1]) { - if (compressor.option("dead_code")) { - var a = []; - if (self.init instanceof AST_Statement) { - a.push(self.init); - } - else if (self.init) { - a.push(make_node(AST_SimpleStatement, self.init, { - body: self.init - })); - } - extract_declarations_from_unreachable_code(compressor, self.body, a); - return make_node(AST_BlockStatement, self, { body: a }); + if (self.condition) { + var cond = self.condition.evaluate(compressor); + if (compressor.option("dead_code") && !cond) { + var a = []; + if (self.init instanceof AST_Statement) { + a.push(self.init); } + else if (self.init) { + a.push(make_node(AST_SimpleStatement, self.init, { + body: self.init + })); + } + extract_declarations_from_unreachable_code(compressor, self.body, a); + return make_node(AST_BlockStatement, self, { body: a }); + } + if (cond !== self.condition) { + cond = make_node_from_constant(cond, self.condition).transform(compressor); + self.condition = best_of_expression(cond, self.condition); } } if_break_in_loop(self, compressor); @@ -2340,9 +2340,8 @@ merge(Compressor.prototype, { // “has no side effects”; also it doesn't work for cases like // `x && true`, though it probably should. var cond = self.condition.evaluate(compressor); - self.condition = cond[0]; - if (cond.length > 1) { - if (cond[1]) { + if (cond !== self.condition) { + if (cond) { compressor.warn("Condition always true [{file}:{line},{col}]", self.condition.start); if (compressor.option("dead_code")) { var a = []; @@ -2361,6 +2360,8 @@ merge(Compressor.prototype, { return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } } + cond = make_node_from_constant(cond, self.condition).transform(compressor); + self.condition = best_of_expression(cond, self.condition); } var negated = self.condition.negate(compressor); var self_condition_length = self.condition.print_to_string().length; @@ -2488,12 +2489,12 @@ merge(Compressor.prototype, { } break; } - var exp = self.expression.evaluate(compressor); - out: if (exp.length == 2) try { + var value = self.expression.evaluate(compressor); + out: if (value !== self.expression) try { // constant expression - self.expression = exp[0]; + var expression = make_node_from_constant(value, self.expression); + self.expression = best_of_expression(expression, self.expression); if (!compressor.option("dead_code")) break out; - var value = exp[1]; var in_if = false; var in_block = false; var started = false; @@ -2540,11 +2541,11 @@ merge(Compressor.prototype, { if (stopped) return MAP.skip; if (node instanceof AST_Case) { var exp = node.expression.evaluate(compressor); - if (exp.length < 2) { + if (exp === node.expression) { // got a case with non-constant expression, baling out throw self; } - if (exp[1] === value || started) { + if (exp === value || started) { started = true; if (aborts(node)) stopped = true; descend(node, this); @@ -2758,15 +2759,14 @@ merge(Compressor.prototype, { var separator; if (self.args.length > 0) { separator = self.args[0].evaluate(compressor); - if (separator.length < 2) break EXIT; // not a constant - separator = separator[1]; + if (separator === self.args[0]) break EXIT; // not a constant } var elements = []; var consts = []; exp.expression.elements.forEach(function(el) { - el = el.evaluate(compressor); - if (el.length > 1) { - consts.push(el[1]); + var value = el.evaluate(compressor); + if (value !== el) { + consts.push(value); } else { if (consts.length > 0) { elements.push(make_node(AST_String, self, { @@ -2774,7 +2774,7 @@ merge(Compressor.prototype, { })); consts.length = 0; } - elements.push(el[0]); + elements.push(el); } }); if (consts.length > 0) { @@ -2815,7 +2815,7 @@ merge(Compressor.prototype, { node.expression = node.expression.clone(); node.expression.expression = node.expression.expression.clone(); node.expression.expression.elements = elements; - return best_of(self, node); + return best_of(compressor, self, node); } } if (exp instanceof AST_Function) { @@ -2961,8 +2961,7 @@ merge(Compressor.prototype, { return e.expression; } if (e instanceof AST_Binary) { - var statement = first_in_statement(compressor); - self = (statement ? best_of_statement : best_of)(self, e.negate(compressor, statement)); + self = best_of(compressor, self, e.negate(compressor, first_in_statement(compressor))); } break; case "typeof": @@ -2975,7 +2974,15 @@ merge(Compressor.prototype, { }).optimize(compressor); } } - return self.evaluate(compressor)[0]; + // avoids infinite recursion of numerals + if (self.operator != "-" || !(self.expression instanceof AST_Number)) { + var ev = self.evaluate(compressor); + if (ev !== self) { + ev = make_node_from_constant(ev, self).optimize(compressor); + return best_of(compressor, ev, self); + } + } + return self; }); function has_side_effects_or_prop_access(node, compressor) { @@ -3097,48 +3104,48 @@ merge(Compressor.prototype, { case "&&": var ll = self.left.evaluate(compressor); var rr = self.right.evaluate(compressor); - if ((ll.length > 1 && !ll[1]) || (rr.length > 1 && !rr[1])) { + if (!ll || !rr) { compressor.warn("Boolean && always false [{file}:{line},{col}]", self.start); return make_node(AST_Seq, self, { car: self.left, cdr: make_node(AST_False, self) }).optimize(compressor); } - if (ll.length > 1 && ll[1]) { - return rr[0]; + if (ll !== self.left && ll) { + return self.right.optimize(compressor); } - if (rr.length > 1 && rr[1]) { - return ll[0]; + if (rr !== self.right && rr) { + return self.left.optimize(compressor); } break; case "||": var ll = self.left.evaluate(compressor); var rr = self.right.evaluate(compressor); - if ((ll.length > 1 && ll[1]) || (rr.length > 1 && rr[1])) { + if (ll !== self.left && ll || rr !== self.right && rr) { compressor.warn("Boolean || always true [{file}:{line},{col}]", self.start); return make_node(AST_Seq, self, { car: self.left, cdr: make_node(AST_True, self) }).optimize(compressor); } - if (ll.length > 1 && !ll[1]) { - return rr[0]; + if (!ll) { + return self.right.optimize(compressor); } - if (rr.length > 1 && !rr[1]) { - return ll[0]; + if (!rr) { + return self.left.optimize(compressor); } break; case "+": var ll = self.left.evaluate(compressor); var rr = self.right.evaluate(compressor); - if (ll.length > 1 && ll[0] instanceof AST_String && ll[1]) { + if (ll && typeof ll == "string") { compressor.warn("+ in boolean context always true [{file}:{line},{col}]", self.start); return make_node(AST_Seq, self, { car: self.right, cdr: make_node(AST_True, self) }).optimize(compressor); } - if (rr.length > 1 && rr[0] instanceof AST_String && rr[1]) { + if (rr && typeof rr == "string") { compressor.warn("+ in boolean context always true [{file}:{line},{col}]", self.start); return make_node(AST_Seq, self, { car: self.left, @@ -3150,12 +3157,11 @@ merge(Compressor.prototype, { if (compressor.option("comparisons") && self.is_boolean()) { if (!(compressor.parent() instanceof AST_Binary) || compressor.parent() instanceof AST_Assign) { - var statement = first_in_statement(compressor); var negated = make_node(AST_UnaryPrefix, self, { operator: "!", - expression: self.negate(compressor, statement) + expression: self.negate(compressor, first_in_statement(compressor)) }); - self = (statement ? best_of_statement : best_of)(self, negated); + self = best_of(compressor, self, negated); } if (compressor.option("unsafe_comps")) { switch (self.operator) { @@ -3307,9 +3313,9 @@ merge(Compressor.prototype, { }); if (self.right instanceof AST_Constant && !(self.left instanceof AST_Constant)) { - self = best_of(reversed, self); + self = best_of(compressor, reversed, self); } else { - self = best_of(self, reversed); + self = best_of(compressor, self, reversed); } } if (associative && self.is_number(compressor)) { @@ -3406,7 +3412,12 @@ merge(Compressor.prototype, { self.right = self.right.right; return self.transform(compressor); } - return self.evaluate(compressor)[0]; + var ev = self.evaluate(compressor); + if (ev !== self) { + ev = make_node_from_constant(ev, self).optimize(compressor); + return best_of(compressor, ev, self); + } + return self; }); OPT(AST_SymbolRef, function(self, compressor){ @@ -3421,11 +3432,11 @@ merge(Compressor.prototype, { && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { switch (self.name) { case "undefined": - return make_node(AST_Undefined, self).transform(compressor); + return make_node(AST_Undefined, self).optimize(compressor); case "NaN": - return make_node(AST_NaN, self).transform(compressor); + return make_node(AST_NaN, self).optimize(compressor); case "Infinity": - return make_node(AST_Infinity, self).transform(compressor); + return make_node(AST_Infinity, self).optimize(compressor); } } if (compressor.option("evaluate") && compressor.option("reduce_vars")) { @@ -3433,12 +3444,14 @@ merge(Compressor.prototype, { if (d.fixed) { if (d.should_replace === undefined) { var init = d.fixed.evaluate(compressor); - if (init.length > 1) { - var value = init[0].print_to_string().length; + if (init !== d.fixed) { + init = make_node_from_constant(init, d.fixed).optimize(compressor); + init = best_of_expression(init, d.fixed); + var value = init.print_to_string().length; var name = d.name.length; var freq = d.references.length; var overhead = d.global || !freq ? 0 : (name + 2 + value) / freq; - d.should_replace = value <= name + overhead ? init[0] : false; + d.should_replace = value <= name + overhead ? init : false; } else { d.should_replace = false; } @@ -3509,8 +3522,8 @@ merge(Compressor.prototype, { return AST_Seq.cons(car, self); } var cond = self.condition.evaluate(compressor); - if (cond.length > 1) { - if (cond[1]) { + if (cond !== self.condition) { + if (cond) { compressor.warn("Condition always true [{file}:{line},{col}]", self.start); return maintain_this_binding(compressor.parent(), self, self.consequent); } else { @@ -3518,9 +3531,8 @@ merge(Compressor.prototype, { return maintain_this_binding(compressor.parent(), self, self.alternative); } } - var statement = first_in_statement(compressor); - var negated = cond[0].negate(compressor, statement); - if ((statement ? best_of_statement : best_of)(cond[0], negated) === negated) { + var negated = cond.negate(compressor, first_in_statement(compressor)); + if (best_of(compressor, cond, negated) === negated) { self = make_node(AST_Conditional, self, { condition: negated, consequent: self.alternative, @@ -3700,7 +3712,12 @@ merge(Compressor.prototype, { }); } } - return self.evaluate(compressor)[0]; + var ev = self.evaluate(compressor); + if (ev !== self) { + ev = make_node_from_constant(ev, self).optimize(compressor); + return best_of(compressor, ev, self); + } + return self; }); OPT(AST_Dot, function(self, compressor){ @@ -3739,13 +3756,17 @@ merge(Compressor.prototype, { break; } } - return self.evaluate(compressor)[0]; + var ev = self.evaluate(compressor); + if (ev !== self) { + ev = make_node_from_constant(ev, self).optimize(compressor); + return best_of(compressor, ev, self); + } + return self; }); function literals_in_boolean_context(self, compressor) { if (compressor.option("booleans") && compressor.in_boolean_context()) { - var best = first_in_statement(compressor) ? best_of_statement : best_of; - return best(self, make_node(AST_Seq, self, { + return best_of(compressor, self, make_node(AST_Seq, self, { car: self, cdr: make_node(AST_True, self) }).optimize(compressor)); From a80b228d8be37eb6585bca01c6fb5468db5bea42 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 16 Mar 2017 12:03:30 +0800 Subject: [PATCH 50/53] fix `hoist_vars` on `reduce_vars` (#1607) `hoist_vars` converts variable declarations into plain assignments, which then confuses `reduce_vars` fixes #1606 --- lib/compress.js | 6 ++++-- test/compress/reduce_vars.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index b3004fb5..49b618e7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1986,7 +1986,7 @@ merge(Compressor.prototype, { vars.set(def.name.name, def); ++vars_found; }); - var seq = node.to_assignments(); + var seq = node.to_assignments(compressor); var p = tt.parent(); if (p instanceof AST_ForIn && p.init === node) { if (seq == null) { @@ -2579,7 +2579,8 @@ merge(Compressor.prototype, { this.definitions.forEach(function(def){ def.value = null }); }); - AST_Definitions.DEFMETHOD("to_assignments", function(){ + AST_Definitions.DEFMETHOD("to_assignments", function(compressor){ + var reduce_vars = compressor.option("reduce_vars"); var assignments = this.definitions.reduce(function(a, def){ if (def.value) { var name = make_node(AST_SymbolRef, def.name, def.name); @@ -2588,6 +2589,7 @@ merge(Compressor.prototype, { left : name, right : def.value })); + if (reduce_vars) name.definition().fixed = false; } return a; }, []); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index a5ab59f9..bc6c72d4 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1327,3 +1327,27 @@ issue_1595_4: { })(3, 4, 5); } } + +issue_1606: { + options = { + evaluate: true, + hoist_vars: true, + reduce_vars: true, + } + input: { + function f() { + var a; + function g(){}; + var b = 2; + x(b); + } + } + expect: { + function f() { + var a, b; + function g(){}; + b = 2; + x(b); + } + } +} From 5ae04b35452693e886a7f836e62e3290b08016a1 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 16 Mar 2017 13:22:26 +0800 Subject: [PATCH 51/53] make `collapse_vars` consistent with `toplevel` (#1608) fixes #1605 --- lib/compress.js | 4 ++- test/compress/collapse_vars.js | 47 +++++++++++++++++++++++++++++++++- test/compress/issue-973.js | 1 + test/mocha/cli.js | 2 +- test/mocha/minify.js | 1 + 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 49b618e7..dac1f364 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -521,6 +521,7 @@ merge(Compressor.prototype, { var self = compressor.self(); var var_defs_removed = false; + var toplevel = compressor.option("toplevel"); for (var stat_index = statements.length; --stat_index >= 0;) { var stat = statements[stat_index]; if (stat instanceof AST_Definitions) continue; @@ -558,7 +559,8 @@ merge(Compressor.prototype, { // Only interested in cases with just one reference to the variable. var def = self.find_variable && self.find_variable(var_name); - if (!def || !def.references || def.references.length !== 1 || var_name == "arguments") { + if (!def || !def.references || def.references.length !== 1 + || var_name == "arguments" || (!toplevel && def.global)) { side_effects_encountered = true; continue; } diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 6d7e2d9f..558a9ee0 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1152,7 +1152,8 @@ collapse_vars_arguments: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + toplevel:true } input: { var outer = function() { @@ -1335,6 +1336,7 @@ issue_1537: { issue_1562: { options = { collapse_vars: true, + toplevel: true, } input: { var v = 1, B = 2; @@ -1363,3 +1365,46 @@ issue_1562: { for (; f(z + 2) ;) bar(30); } } + +issue_1605_1: { + options = { + collapse_vars: true, + toplevel: false, + } + input: { + function foo(x) { + var y = x; + return y; + } + var o = new Object; + o.p = 1; + } + expect: { + function foo(x) { + return x; + } + var o = new Object; + o.p = 1; + } +} + +issue_1605_2: { + options = { + collapse_vars: true, + toplevel: "vars", + } + input: { + function foo(x) { + var y = x; + return y; + } + var o = new Object; + o.p = 1; + } + expect: { + function foo(x) { + return x; + } + (new Object).p = 1; + } +} diff --git a/test/compress/issue-973.js b/test/compress/issue-973.js index 0e040922..30f886a8 100644 --- a/test/compress/issue-973.js +++ b/test/compress/issue-973.js @@ -50,6 +50,7 @@ this_binding_conditionals: { this_binding_collapse_vars: { options = { collapse_vars: true, + toplevel: true, }; input: { var c = a; c(); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index fa952d91..2b44c901 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -152,7 +152,7 @@ describe("bin/uglifyjs", function () { }); }); it("Should process inline source map", function(done) { - var command = uglifyjscmd + ' test/input/issue-520/input.js -cm toplevel --in-source-map inline --source-map-inline'; + var command = uglifyjscmd + ' test/input/issue-520/input.js -mc toplevel --in-source-map inline --source-map-inline'; exec(command, function (err, stdout) { if (err) throw err; diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 51c46b28..a4587cb7 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -78,6 +78,7 @@ describe("minify", function() { }); it("Should process inline source map", function() { var code = Uglify.minify("./test/input/issue-520/input.js", { + compress: { toplevel: true }, inSourceMap: "inline", sourceMapInline: true }).code + "\n"; From 3563d8c09e36be8f8b9cb9500852778f8d191d5d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 16 Mar 2017 23:20:06 +0800 Subject: [PATCH 52/53] extend `test/run-tests.js` to optionally execute uglified output (#1604) fixes #1588 --- test/compress/issue-1588.js | 87 +++++++++++++++++++++++++++++++++++++ test/run-tests.js | 85 ++++++++++++++++++++++++++++++++---- 2 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 test/compress/issue-1588.js diff --git a/test/compress/issue-1588.js b/test/compress/issue-1588.js new file mode 100644 index 00000000..ff25635c --- /dev/null +++ b/test/compress/issue-1588.js @@ -0,0 +1,87 @@ +screw_ie8: { + options = { + screw_ie8: true, + } + mangle = { + screw_ie8: true, + } + input: { + try { throw "foo"; } catch (x) { console.log(x); } + } + expect_exact: 'try{throw"foo"}catch(o){console.log(o)}' + expect_stdout: [ + "foo" + ] +} + +support_ie8: { + options = { + screw_ie8: false, + } + mangle = { + screw_ie8: false, + } + input: { + try { throw "foo"; } catch (x) { console.log(x); } + } + expect_exact: 'try{throw"foo"}catch(x){console.log(x)}' + expect_stdout: "foo" +} + +safe_undefined: { + options = { + conditionals: true, + if_return: true, + unsafe: false, + } + mangle = {} + input: { + var a, c; + console.log(function(undefined) { + return function() { + if (a) + return b; + if (c) + return d; + }; + }(1)()); + } + expect: { + var a, c; + console.log(function(n) { + return function() { + return a ? b : c ? d : void 0; + }; + }(1)()); + } + expect_stdout: true +} + +unsafe_undefined: { + options = { + conditionals: true, + if_return: true, + unsafe: true, + } + mangle = {} + input: { + var a, c; + console.log(function(undefined) { + return function() { + if (a) + return b; + if (c) + return d; + }; + }()()); + } + expect: { + var a, c; + console.log(function(n) { + return function() { + return a ? b : c ? d : n; + }; + }()()); + } + expect_stdout: true +} diff --git a/test/run-tests.js b/test/run-tests.js index 36d26ef7..898bb793 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -6,6 +6,7 @@ var U = require("../tools/node"); var path = require("path"); var fs = require("fs"); var assert = require("assert"); +var vm = require("vm"); var tests_dir = path.dirname(module.filename); var failures = 0; @@ -165,6 +166,51 @@ function run_compress_tests() { failed_files[file] = 1; } } + if (test.expect_stdout) { + try { + var stdout = run_code(input_code); + if (test.expect_stdout === true) { + test.expect_stdout = stdout; + } + if (test.expect_stdout != stdout) { + log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED STDOUT---\n{expected}\n---ACTUAL STDOUT---\n{actual}\n\n", { + input: input_code, + expected: test.expect_stdout, + actual: stdout, + }); + failures++; + failed_files[file] = 1; + } else { + try { + stdout = run_code(output); + if (test.expect_stdout != stdout) { + log("!!! failed\n---INPUT---\n{input}\n---EXPECTED STDOUT---\n{expected}\n---ACTUAL STDOUT---\n{actual}\n\n", { + input: input_code, + expected: test.expect_stdout, + actual: stdout, + }); + failures++; + failed_files[file] = 1; + } + } catch (ex) { + log("!!! Execution of output failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--ERROR--\n{error}\n\n", { + input: input_code, + output: output, + error: ex.toString(), + }); + failures++; + failed_files[file] = 1; + } + } + } catch (ex) { + log("!!! Execution of input failed\n---INPUT---\n{input}\n--ERROR--\n{error}\n\n", { + input: input_code, + error: ex.toString(), + }); + failures++; + failed_files[file] = 1; + } + } } } var tests = parse_test(path.resolve(dir, file)); @@ -215,9 +261,9 @@ function parse_test(file) { } function read_string(stat) { - if (stat.TYPE === "SimpleStatement") { + if (stat.TYPE == "SimpleStatement") { var body = stat.body; - out: switch(body.TYPE) { + switch(body.TYPE) { case "String": return body.value; case "Array": @@ -243,12 +289,13 @@ function parse_test(file) { return true; } if (node instanceof U.AST_LabeledStatement) { + var label = node.label; assert.ok( - ["input", "expect", "expect_exact", "expect_warnings"].indexOf(node.label.name) >= 0, + ["input", "expect", "expect_exact", "expect_warnings", "expect_stdout"].indexOf(label.name) >= 0, tmpl("Unsupported label {name} [{line},{col}]", { - name: node.label.name, - line: node.label.start.line, - col: node.label.start.col + name: label.name, + line: label.start.line, + col: label.start.col }) ); var stat = node.body; @@ -256,10 +303,16 @@ function parse_test(file) { if (stat.body.length == 1) stat = stat.body[0]; else if (stat.body.length == 0) stat = new U.AST_EmptyStatement(); } - if (node.label.name === "expect_exact") { - test[node.label.name] = read_string(stat); + if (label.name == "expect_exact") { + test[label.name] = read_string(stat); + } else if (label.name == "expect_stdout") { + if (stat.TYPE == "SimpleStatement" && stat.body instanceof U.AST_Boolean) { + test[label.name] = stat.body.value; + } else { + test[label.name] = read_string(stat) + "\n"; + } } else { - test[node.label.name] = stat; + test[label.name] = stat; } return true; } @@ -281,3 +334,17 @@ function evaluate(code) { code = make_code(code, { beautify: true }); return new Function("return(" + code + ")")(); } + +function run_code(code) { + var stdout = ""; + var original_write = process.stdout.write; + process.stdout.write = function(chunk) { + stdout += chunk; + }; + try { + new vm.Script(code).runInNewContext({ console: console }, { timeout: 5000 }); + return stdout; + } finally { + process.stdout.write = original_write; + } +} From ac403018135b0ba700ef6223970c1bbc2a518107 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 17 Mar 2017 00:26:48 +0800 Subject: [PATCH 53/53] fix chained evaluation (#1610) `reduce_vars` enables substitution of variables but did not clone the value's `AST_Node`. This confuses `collapse_vars` and result in invalid AST and subsequent crash. fixes #1609 --- lib/compress.js | 2 +- test/compress/issue-1609.js | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1609.js diff --git a/lib/compress.js b/lib/compress.js index dac1f364..c3f12549 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3461,7 +3461,7 @@ merge(Compressor.prototype, { } } if (d.should_replace) { - return d.should_replace; + return d.should_replace.clone(true); } } } diff --git a/test/compress/issue-1609.js b/test/compress/issue-1609.js new file mode 100644 index 00000000..577a3ee1 --- /dev/null +++ b/test/compress/issue-1609.js @@ -0,0 +1,56 @@ +chained_evaluation_1: { + options = { + collapse_vars: true, + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + (function() { + var a = 1; + (function() { + var b = a, c; + c = f(b); + c.bar = b; + })(); + })(); + } + expect: { + (function() { + (function() { + var c; + c = f(1); + c.bar = 1; + })(); + })(); + } +} + +chained_evaluation_2: { + options = { + collapse_vars: true, + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + (function() { + var a = "long piece of string"; + (function() { + var b = a, c; + c = f(b); + c.bar = b; + })(); + })(); + } + expect: { + (function() { + var a = "long piece of string"; + (function() { + var c; + c = f(a); + c.bar = a; + })(); + })(); + } +}