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 01/14] 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 79064d79c32..628bcdecf81 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 38ebbf403c8..ec1e71748fd 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 8e32ad02a75..0b40bb9c88d 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 02/14] 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 628bcdecf81..0b532a839b6 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 ec1e71748fd..2cd7912880b 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 4b61318109d..20dab3b9e46 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 26b6e489825..68739503ebd 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 d3d99b57a18..18505a184b6 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 00000000000..dd3f3f2196a --- /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 f17ae206ce9..9a0b5a46867 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 d93f5237259..7bb274cb552 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 03/14] 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 2cd7912880b..35459006fa0 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 04/14] 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 367d66e2cb0..4dd234dcc8c 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 f152a51e68a..4512eb711a2 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 05/14] 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 35459006fa0..7eead2c196c 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 18505a184b6..a1a515a13d0 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 06/14] 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 4a0a1e0e1a6..10fed135b43 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 9b198ccdd83..9aadc9f5a08 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 01580c87dbf..9474e73208b 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 07/14] 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 0b532a839b6..f880fd7ba79 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 4dd234dcc8c..e39a4b4bf27 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 7eead2c196c..4a5f6395c68 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 ae0c5777718..b00fcb4a13d 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 31c448fd091..51379b159c2 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 08/14] 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 4a5f6395c68..f2269a2f953 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 a2565872d0c..304a71ac0ce 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 7bb274cb552..41cfc726159 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 09/14] 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 f3df78fec63..1f163304d4c 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 f2269a2f953..1a54c75e69d 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 b00fcb4a13d..f23c8fe2a26 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 557631bd19a..70e915d3283 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 51379b159c2..36eb4d3a1da 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 10/14] 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 1a54c75e69d..f1409d90561 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,21 +2829,35 @@ 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 (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 (!self.car.has_side_effects(compressor) - && self.car.equivalent_to(self.cdr)) { - return self.car; - } } if (is_undefined(self.cdr)) { return make_node(AST_UnaryPrefix, self, { diff --git a/test/compress/issue-368.js b/test/compress/issue-368.js new file mode 100644 index 00000000000..8c41a894501 --- /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 11/14] v2.8.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4512eb711a2..46f5eaec540 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 12/14] 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 f1409d90561..d9a67c162cf 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 8c41a894501..5960aa64ab4 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 13/14] v2.8.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46f5eaec540..31aaf9515d1 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 33a26d456be26341ada729056c2f02882eb9ae4c Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 5 Mar 2017 16:15:13 +0800 Subject: [PATCH 14/14] patch up #1543 for harmony fixes #1537 --- lib/compress.js | 2 + test/compress/collapse_vars.js | 78 ++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index dfc7e21f405..1442113ae17 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -549,6 +549,7 @@ merge(Compressor.prototype, { if (var_decl.value.is_constant()) { var ctt = new TreeTransformer(function(node) { if (node === ref + && !ctt.find_parent(AST_Destructuring) && !ctt.find_parent(AST_ForIn)) { return replace_var(node, ctt.parent(), true); } @@ -581,6 +582,7 @@ merge(Compressor.prototype, { if (unwind) return node; var parent = tt.parent(); if (node instanceof AST_Lambda + || node instanceof AST_Destructuring || node instanceof AST_Try || node instanceof AST_With || node instanceof AST_Case diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index c8fa046051b..6d9961ff463 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1343,3 +1343,81 @@ issue_1537_for_of: { for (k of {prop: 'val'}); } } + +issue_1537_destructuring_1: { + options = { + collapse_vars: true, + } + input: { + var x = 1, y = 2; + [x] = [y]; + } + expect: { + var x = 1; + [x] = [2]; + } +} + +issue_1537_destructuring_2: { + options = { + collapse_vars: true, + } + input: { + var x = foo(); + [x] = [1]; + } + expect: { + var x = foo(); + [x] = [1]; + } +} + +issue_1537_destructuring_3: { + options = { + collapse_vars: true, + } + input: { + var x = Math.random(); + ({p: x = 9} = {v: 1}); + } + expect: { + var x = Math.random(); + ({p: x = 9} = {v: 1}); + } +} + +issue_1537_destructuring_for_in: { + options = { + collapse_vars: true, + } + input: { + var x = 1, y = 2; + (function() { + for ([[x], y] in a); + })(); + } + expect: { + var x = 1, y = 2; + (function() { + for ([[x], y] in a); + })(); + } +} + +issue_1537_destructuring_for_of: { + options = { + collapse_vars: true, + } + input: { + var x = 1, y = 2; + (function() { + for ([[x], y] of a); + })(); + } + expect: { + var x = 1, y = 2; + (function() { + for ([[x], y] of a); + })(); + } +}