From 6fa3fbeae84200c90ff47dde03545742a861be17 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 29 Jan 2018 15:13:25 +0800 Subject: [PATCH 01/25] compress chained compound assignments (#2850) --- lib/compress.js | 63 ++++++++++++++++++----- test/compress/collapse_vars.js | 11 ++-- test/compress/reduce_vars.js | 94 ++++++++++++++++++++++++++-------- 3 files changed, 125 insertions(+), 43 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 07128aff..81af151b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -320,6 +320,7 @@ merge(Compressor.prototype, { function reset_def(compressor, def) { def.assignments = 0; + def.chained = false; def.direct_access = false; def.escaped = false; if (def.scope.uses_eval || def.scope.uses_with) { @@ -485,19 +486,27 @@ merge(Compressor.prototype, { }); def(AST_Assign, function(tw) { var node = this; - if (node.operator != "=" || !(node.left instanceof AST_SymbolRef)) return; + if (!(node.left instanceof AST_SymbolRef)) return; var d = node.left.definition(); - if (safe_to_assign(tw, d, node.right)) { - d.references.push(node.left); - d.assignments++; - d.fixed = function() { - return node.right; - }; - mark(tw, d, false); - node.right.walk(tw); - mark(tw, d, true); - return true; - } + var fixed = d.fixed; + if (!fixed && node.operator != "=") return; + if (!safe_to_assign(tw, d, node.right)) return; + d.references.push(node.left); + d.assignments++; + if (node.operator != "=") d.chained = true; + d.fixed = node.operator == "=" ? function() { + return node.right; + } : function() { + return make_node(AST_Binary, node, { + operator: node.operator.slice(0, -1), + left: fixed instanceof AST_Node ? fixed : fixed(), + right: node.right + }); + }; + mark(tw, d, false); + node.right.walk(tw); + mark(tw, d, true); + return true; }); def(AST_Binary, function(tw) { if (!lazy_op(this.operator)) return; @@ -672,6 +681,32 @@ merge(Compressor.prototype, { if (this.bfinally) this.bfinally.walk(tw); return true; }); + def(AST_Unary, function(tw, descend) { + var node = this; + if (node.operator != "++" && node.operator != "--") return; + if (!(node.expression instanceof AST_SymbolRef)) return; + var d = node.expression.definition(); + var fixed = d.fixed; + if (!fixed) return; + if (!safe_to_assign(tw, d, true)) return; + d.references.push(node.expression); + d.assignments++; + d.chained = true; + d.fixed = function() { + return make_node(AST_Binary, node, { + operator: node.operator.slice(0, -1), + left: make_node(AST_UnaryPrefix, node, { + operator: "+", + expression: fixed instanceof AST_Node ? fixed : fixed() + }), + right: make_node(AST_Number, node, { + value: 1 + }) + }); + }; + mark(tw, d, true); + return true; + }); def(AST_VarDef, function(tw, descend) { var node = this; var d = node.name.definition(); @@ -2946,7 +2981,7 @@ merge(Compressor.prototype, { if (def.value.has_side_effects(compressor)) { def.value.walk(tw); } - if (def.name.fixed_value() === def.value) { + if (!node_def.chained && def.name.fixed_value() === def.value) { fixed_ids[node_def.id] = def; } } @@ -3164,7 +3199,7 @@ merge(Compressor.prototype, { && self.variables.get(sym.name) === (node_def = sym.definition())) { if (node instanceof AST_Assign) { node.right.walk(tw); - if (node.left.fixed_value() === node.right) { + if (!node_def.chained && node.left.fixed_value() === node.right) { fixed_ids[node_def.id] = node; } } diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 970f822e..4172b33b 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -52,13 +52,8 @@ collapse_vars_side_effects_1: { console.log.bind(console)(s.charAt(i++), s.charAt(i++), s.charAt(i++), 7); } function f2() { - var log = console.log.bind(console), - s = "abcdef", - i = 2, - x = s.charAt(i++), - y = s.charAt(i++), - z = s.charAt(i++); - log(x, i, y, z, 7); + var s = "abcdef", i = 2; + console.log.bind(console)(s.charAt(i++), 5, s.charAt(i++), s.charAt(i++), 7); } function f3() { var s = "abcdef", @@ -72,7 +67,7 @@ collapse_vars_side_effects_1: { var i = 10, x = i += 2, y = i += 3; - console.log.bind(console)(x, i += 4, y, i); + console.log.bind(console)(x, i += 4, y, 19); } f1(), f2(), f3(), f4(); } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 4009c35b..2231587d 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -76,14 +76,12 @@ modified: { console.log(a + 1); console.log(b + 1); } - function f1() { var a = 1, b = 2; --b; console.log(a + 1); console.log(b + 1); } - function f2() { var a = 1, b = 2, c = 3; b = c; @@ -92,7 +90,6 @@ modified: { console.log(a + c); console.log(a + b + c); } - function f3() { var a = 1, b = 2, c = 3; b *= c; @@ -101,7 +98,6 @@ modified: { console.log(a + c); console.log(a + b + c); } - function f4() { var a = 1, b = 2, c = 3; if (a) { @@ -114,28 +110,26 @@ modified: { console.log(a + c); console.log(a + b + c); } - function f5(a) { B = a; - console.log(A ? 'yes' : 'no'); - console.log(B ? 'yes' : 'no'); + console.log(typeof A ? "yes" : "no"); + console.log(typeof B ? "yes" : "no"); } + f0(), f1(), f2(), f3(), f4(), f5(); } expect: { function f0() { var b = 2; b++; console.log(2); - console.log(b + 1); + console.log(4); } - function f1() { var b = 2; --b; console.log(2); - console.log(b + 1); + console.log(2); } - function f2() { 3; console.log(4); @@ -143,16 +137,14 @@ modified: { console.log(4); console.log(7); } - function f3() { var b = 2; b *= 3; - console.log(1 + b); - console.log(b + 3); + console.log(7); + console.log(9); console.log(4); - console.log(1 + b + 3); + console.log(10); } - function f4() { var b = 2, c = 3; b = c; @@ -161,13 +153,33 @@ modified: { console.log(1 + c); console.log(1 + b + c); } - function f5(a) { B = a; - console.log(A ? 'yes' : 'no'); - console.log(B ? 'yes' : 'no'); + console.log(typeof A ? "yes" : "no"); + console.log(typeof B ? "yes" : "no"); } + f0(), f1(), f2(), f3(), f4(), f5(); } + expect_stdout: [ + "2", + "4", + "2", + "2", + "4", + "6", + "4", + "7", + "7", + "9", + "4", + "10", + "4", + "6", + "4", + "7", + "yes", + "yes", + ] } unsafe_evaluate: { @@ -745,7 +757,7 @@ iife: { expect: { !function(a, b, c) { b++; - console.log(0, 1 * b, 5); + console.log(0, 3, 5); }(1, 2, 3); } expect_stdout: true @@ -766,7 +778,7 @@ iife_new: { expect: { var A = new function(a, b, c) { b++; - console.log(0, 1 * b, 5); + console.log(0, 3, 5); }(1, 2, 3); } expect_stdout: true @@ -5383,3 +5395,43 @@ issue_2836: { } expect_stdout: "PASS" } + +lvalues_def_1: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var b = 1; + var a = b++, b = NaN; + console.log(a, b); + } + expect: { + var b = 1; + var a = b++; + b = NaN; + console.log(a, b); + } + expect_stdout: "1 NaN" +} + +lvalues_def_2: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var b = 1; + var a = b += 1, b = NaN; + console.log(a, b); + } + expect: { + var b = 1; + var a = b += 1; + b = NaN; + console.log(a, b); + } + expect_stdout: "2 NaN" +} From 541e6011af7647074a41c5a82a0a16a3d32bb8c6 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 29 Jan 2018 17:41:15 +0800 Subject: [PATCH 02/25] improve symbol replacement heuristic (#2851) --- lib/compress.js | 2 +- test/compress/reduce_vars.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 81af151b..d0ef5f5d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -5237,7 +5237,7 @@ merge(Compressor.prototype, { var name_length = d.name.length; var overhead = 0; if (compressor.option("unused") && !compressor.exposed(d)) { - overhead = (name_length + 2 + value_length) / d.references.length; + overhead = (name_length + 2 + value_length) / (d.references.length - d.assignments); } d.should_replace = value_length <= name_length + overhead ? fn : false; } else { diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 2231587d..2e562f94 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -5435,3 +5435,35 @@ lvalues_def_2: { } expect_stdout: "2 NaN" } + +chained_assignments: { + options = { + evaluate: true, + inline: true, + reduce_vars: true, + sequences: true, + side_effects: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + function f() { + var a = [0x5e, 0xad, 0xbe, 0xef]; + var b = 0; + b |= a[0]; + b <<= 8; + b |= a[1]; + b <<= 8; + b |= a[2]; + b <<= 8; + b |= a[3]; + return b; + } + console.log(f().toString(16)); + } + expect: { + console.log("5eadbeef"); + } + expect_stdout: "5eadbeef" +} From 2a4c68be4f3ed60fa638b66a57e7056f7507da83 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 31 Jan 2018 23:49:59 +0800 Subject: [PATCH 03/25] relax `collapse_vars` on `AST_Exit` (#2855) First introduced in #1862 to stop assignments to migrate beyond `return` or `throw`. Since then `collapse_vars` has been improved to handle various side-effect-related corner cases. --- lib/compress.js | 1 - test/compress/collapse_vars.js | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index d0ef5f5d..db6ee8be 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1044,7 +1044,6 @@ merge(Compressor.prototype, { // but are otherwise not safe to scan into or beyond them. var sym; if (node instanceof AST_Call - || node instanceof AST_Exit || node instanceof AST_PropAccess && (side_effects || node.expression.may_throw_on_access(compressor)) || node instanceof AST_SymbolRef diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 4172b33b..b3617816 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -4107,3 +4107,86 @@ unsafe_builtin: { } expect_stdout: "1 4" } + +return_1: { + options = { + collapse_vars: true, + unused: true, + } + input: { + var log = console.log; + function f(b, c) { + var a = c; + if (b) return b; + log(a); + } + f(false, 1); + f(true, 2); + } + expect: { + var log = console.log; + function f(b, c) { + if (b) return b; + log(c); + } + f(false, 1); + f(true, 2); + } + expect_stdout: "1" +} + +return_2: { + options = { + collapse_vars: true, + unused: true, + } + input: { + var log = console.log; + function f(b, c) { + var a = c(); + if (b) return b; + log(a); + } + f(false, function() { return 1 }); + f(true, function() { return 2 }); + } + expect: { + var log = console.log; + function f(b, c) { + var a = c(); + if (b) return b; + log(a); + } + f(false, function() { return 1 }); + f(true, function() { return 2 }); + } + expect_stdout: "1" +} + +return_3: { + options = { + collapse_vars: true, + unused: true, + } + input: { + var log = console.log; + function f(b, c) { + var a = b <<= c; + if (b) return b; + log(a); + } + f(false, 1); + f(true, 2); + } + expect: { + var log = console.log; + function f(b, c) { + var a = b <<= c; + if (b) return b; + log(a); + } + f(false, 1); + f(true, 2); + } + expect_stdout: "0" +} From 102f994b9d09b3ed0447f02a894fd88fcc903171 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 1 Feb 2018 15:09:53 +0800 Subject: [PATCH 04/25] account for declaration assignment in `collapse_vars` (#2859) fixes #2858 --- lib/compress.js | 1 + test/compress/collapse_vars.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index db6ee8be..9a3446c2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -990,6 +990,7 @@ merge(Compressor.prototype, { || node instanceof AST_Debugger || node instanceof AST_IterationStatement && !(node instanceof AST_For) || node instanceof AST_Try + || node instanceof AST_VarDef && node.value && side_effects && !references_in_scope(node.name.definition()) || node instanceof AST_With || parent instanceof AST_For && node !== parent.init || (side_effects || !replace_all) diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index b3617816..f56face6 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -4190,3 +4190,37 @@ return_3: { } expect_stdout: "0" } + +issue_2858: { + options = { + collapse_vars: true, + unused: true, + } + input: { + var b; + (function() { + function f() { + a++; + } + f(); + var c = f(); + var a = void 0; + c || (b = a); + })(); + console.log(b); + } + expect: { + var b; + (function() { + function f() { + a++; + } + f(); + var c = f(); + var a = void 0; + c || (b = a); + })(); + console.log(b); + } + expect_stdout: "undefined" +} From aa664dea0a07656d1fa61263e3a03e7d8b5c4cc9 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 1 Feb 2018 16:18:29 +0800 Subject: [PATCH 05/25] avoid `evaluate` of compound assignment after `dead_code` transform (#2861) fixes #2860 --- lib/compress.js | 1 + test/compress/dead-code.js | 38 ++++++++++++++++++++++++++++++++++ test/compress/reduce_vars.js | 40 ++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 9a3446c2..b4d13ec7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -5358,6 +5358,7 @@ merge(Compressor.prototype, { if (in_try(level, parent instanceof AST_Throw)) break; if (is_reachable(def.scope, [ def ])) break; if (self.operator == "=") return self.right; + def.fixed = false; return make_node(AST_Binary, self, { operator: self.operator.slice(0, -1), left: self.left, diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index b66d5ac1..9b45fe87 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -879,3 +879,41 @@ unsafe_builtin: { z; } } + +issue_2860_1: { + options = { + dead_code: true, + evaluate: true, + reduce_vars: true, + } + input: { + console.log(function(a) { + return a ^= 1; + }()); + } + expect: { + console.log(function(a) { + return 1 ^ a; + }()); + } + expect_stdout: "1" +} + +issue_2860_2: { + options = { + dead_code: true, + evaluate: true, + inline: true, + passes: 2, + reduce_vars: true, + } + input: { + console.log(function(a) { + return a ^= 1; + }()); + } + expect: { + console.log(1); + } + expect_stdout: "1" +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 2e562f94..761af9e2 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -5467,3 +5467,43 @@ chained_assignments: { } expect_stdout: "5eadbeef" } + +issue_2860_1: { + options = { + dead_code: true, + evaluate: true, + reduce_vars: true, + } + input: { + console.log(function(a) { + return a ^= 1; + a ^= 2; + }()); + } + expect: { + console.log(function(a) { + return 1 ^ a; + }()); + } + expect_stdout: "1" +} + +issue_2860_2: { + options = { + dead_code: true, + evaluate: true, + inline: true, + passes: 2, + reduce_vars: true, + } + input: { + console.log(function(a) { + return a ^= 1; + a ^= 2; + }()); + } + expect: { + console.log(1); + } + expect_stdout: "1" +} From fad6766a905350574b698af707958294e22184f3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 1 Feb 2018 16:50:54 +0800 Subject: [PATCH 06/25] simplify comparisons with `undefined` & `null` (#2862) fixes #2857 --- lib/compress.js | 28 ++++++++ test/compress/comparing.js | 130 +++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index b4d13ec7..a88ea2c4 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4788,6 +4788,34 @@ merge(Compressor.prototype, { return make_node(self.operator[0] == "=" ? AST_True : AST_False, self); } break; + case "&&": + case "||": + var lhs = self.left; + if (lhs.operator == self.operator) { + lhs = lhs.right; + } + if (lhs instanceof AST_Binary + && lhs.operator == (self.operator == "&&" ? "!==" : "===") + && self.right instanceof AST_Binary + && lhs.operator == self.right.operator + && (is_undefined(lhs.left, compressor) && self.right.left instanceof AST_Null + || lhs.left instanceof AST_Null && is_undefined(self.right.left, compressor)) + && lhs.right.equivalent_to(self.right.right)) { + var combined = make_node(AST_Binary, self, { + operator: lhs.operator.slice(0, -1), + left: make_node(AST_Null, self), + right: lhs.right + }); + if (lhs !== self.left) { + combined = make_node(AST_Binary, self, { + operator: self.operator, + left: self.left.left, + right: combined + }); + } + return combined; + } + break; } if (self.operator == "+" && compressor.in_boolean_context()) { var ll = self.left.evaluate(compressor); diff --git a/test/compress/comparing.js b/test/compress/comparing.js index e374b585..d581d086 100644 --- a/test/compress/comparing.js +++ b/test/compress/comparing.js @@ -112,3 +112,133 @@ self_comparison_2: { } expect_stdout: "false true" } + +issue_2857_1: { + options = { + comparisons: true, + } + input: { + a === undefined || a === null; + a === undefined || a !== null; + a !== undefined || a === null; + a !== undefined || a !== null; + a === undefined && a === null; + a === undefined && a !== null; + a !== undefined && a === null; + a !== undefined && a !== null; + } + expect: { + null == a; + void 0 === a || null !== a; + void 0 !== a || null === a; + void 0 !== a || null !== a; + void 0 === a && null === a; + void 0 === a && null !== a; + void 0 !== a && null === a; + null != a; + } +} + +issue_2857_2: { + options = { + comparisons: true, + } + input: { + a === undefined || a === null || p; + a === undefined || a !== null || p; + a !== undefined || a === null || p; + a !== undefined || a !== null || p; + a === undefined && a === null || p; + a === undefined && a !== null || p; + a !== undefined && a === null || p; + a !== undefined && a !== null || p; + } + expect: { + null == a || p; + void 0 === a || null !== a || p; + void 0 !== a || null === a || p; + void 0 !== a || null !== a || p; + void 0 === a && null === a || p; + void 0 === a && null !== a || p; + void 0 !== a && null === a || p; + null != a || p; + } +} + +issue_2857_3: { + options = { + comparisons: true, + } + input: { + a === undefined || a === null && p; + a === undefined || a !== null && p; + a !== undefined || a === null && p; + a !== undefined || a !== null && p; + a === undefined && a === null && p; + a === undefined && a !== null && p; + a !== undefined && a === null && p; + a !== undefined && a !== null && p; + } + expect: { + void 0 === a || null === a && p; + void 0 === a || null !== a && p; + void 0 !== a || null === a && p; + void 0 !== a || null !== a && p; + void 0 === a && null === a && p; + void 0 === a && null !== a && p; + void 0 !== a && null === a && p; + null != a && p; + } +} + +issue_2857_4: { + options = { + comparisons: true, + } + input: { + p || a === undefined || a === null; + p || a === undefined || a !== null; + p || a !== undefined || a === null; + p || a !== undefined || a !== null; + p || a === undefined && a === null; + p || a === undefined && a !== null; + p || a !== undefined && a === null; + p || a !== undefined && a !== null; + } + expect: { + p || null == a; + p || void 0 === a || null !== a; + p || void 0 !== a || null === a; + p || void 0 !== a || null !== a; + p || void 0 === a && null === a; + p || void 0 === a && null !== a; + p || void 0 !== a && null === a; + p || null != a; + } +} + +issue_2857_5: { + options = { + comparisons: true, + } + input: { + p && a === undefined || a === null; + p && a === undefined || a !== null; + p && a !== undefined || a === null; + p && a !== undefined || a !== null; + p && a === undefined && a === null; + p && a === undefined && a !== null; + p && a !== undefined && a === null; + p && a !== undefined && a !== null; + } + expect: { + p && void 0 === a || null === a; + p && void 0 === a || null !== a; + p && void 0 !== a || null === a; + p && void 0 !== a || null !== a; + p && void 0 === a && null === a; + p && void 0 === a && null !== a; + p && void 0 !== a && null === a; + p && null != a; + } +} From c3a002ff9739b60ec1836719c1e240df22a67830 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 1 Feb 2018 19:15:17 +0800 Subject: [PATCH 07/25] account for side-effects in `comparisons` of `null` & `undefined` (#2863) --- lib/compress.js | 1 + test/compress/comparing.js | 233 ++++++++++++++++++++++++------------- 2 files changed, 154 insertions(+), 80 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index a88ea2c4..1b0aa15a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4800,6 +4800,7 @@ merge(Compressor.prototype, { && lhs.operator == self.right.operator && (is_undefined(lhs.left, compressor) && self.right.left instanceof AST_Null || lhs.left instanceof AST_Null && is_undefined(self.right.left, compressor)) + && !lhs.right.has_side_effects(compressor) && lhs.right.equivalent_to(self.right.right)) { var combined = make_node(AST_Binary, self, { operator: lhs.operator.slice(0, -1), diff --git a/test/compress/comparing.js b/test/compress/comparing.js index d581d086..d56445e0 100644 --- a/test/compress/comparing.js +++ b/test/compress/comparing.js @@ -118,24 +118,48 @@ issue_2857_1: { comparisons: true, } input: { - a === undefined || a === null; - a === undefined || a !== null; - a !== undefined || a === null; - a !== undefined || a !== null; - a === undefined && a === null; - a === undefined && a !== null; - a !== undefined && a === null; - a !== undefined && a !== null; + function f1(a) { + a === undefined || a === null; + a === undefined || a !== null; + a !== undefined || a === null; + a !== undefined || a !== null; + a === undefined && a === null; + a === undefined && a !== null; + a !== undefined && a === null; + a !== undefined && a !== null; + } + function f2(a) { + a === null || a === undefined; + a === null || a !== undefined; + a !== null || a === undefined; + a !== null || a !== undefined; + a === null && a === undefined; + a === null && a !== undefined; + a !== null && a === undefined; + a !== null && a !== undefined; + } } expect: { - null == a; - void 0 === a || null !== a; - void 0 !== a || null === a; - void 0 !== a || null !== a; - void 0 === a && null === a; - void 0 === a && null !== a; - void 0 !== a && null === a; - null != a; + function f1(a) { + null == a; + void 0 === a || null !== a; + void 0 !== a || null === a; + void 0 !== a || null !== a; + void 0 === a && null === a; + void 0 === a && null !== a; + void 0 !== a && null === a; + null != a; + } + function f2(a) { + null == a; + null === a || void 0 !== a; + null !== a || void 0 === a; + null !== a || void 0 !== a; + null === a && void 0 === a; + null === a && void 0 !== a; + null !== a && void 0 === a; + null != a; + } } } @@ -144,24 +168,28 @@ issue_2857_2: { comparisons: true, } input: { - a === undefined || a === null || p; - a === undefined || a !== null || p; - a !== undefined || a === null || p; - a !== undefined || a !== null || p; - a === undefined && a === null || p; - a === undefined && a !== null || p; - a !== undefined && a === null || p; - a !== undefined && a !== null || p; + function f(a, p) { + a === undefined || a === null || p; + a === undefined || a !== null || p; + a !== undefined || a === null || p; + a !== undefined || a !== null || p; + a === undefined && a === null || p; + a === undefined && a !== null || p; + a !== undefined && a === null || p; + a !== undefined && a !== null || p; + } } expect: { - null == a || p; - void 0 === a || null !== a || p; - void 0 !== a || null === a || p; - void 0 !== a || null !== a || p; - void 0 === a && null === a || p; - void 0 === a && null !== a || p; - void 0 !== a && null === a || p; - null != a || p; + function f(a, p) { + null == a || p; + void 0 === a || null !== a || p; + void 0 !== a || null === a || p; + void 0 !== a || null !== a || p; + void 0 === a && null === a || p; + void 0 === a && null !== a || p; + void 0 !== a && null === a || p; + null != a || p; + } } } @@ -170,24 +198,28 @@ issue_2857_3: { comparisons: true, } input: { - a === undefined || a === null && p; - a === undefined || a !== null && p; - a !== undefined || a === null && p; - a !== undefined || a !== null && p; - a === undefined && a === null && p; - a === undefined && a !== null && p; - a !== undefined && a === null && p; - a !== undefined && a !== null && p; + function f(a, p) { + a === undefined || a === null && p; + a === undefined || a !== null && p; + a !== undefined || a === null && p; + a !== undefined || a !== null && p; + a === undefined && a === null && p; + a === undefined && a !== null && p; + a !== undefined && a === null && p; + a !== undefined && a !== null && p; + } } expect: { - void 0 === a || null === a && p; - void 0 === a || null !== a && p; - void 0 !== a || null === a && p; - void 0 !== a || null !== a && p; - void 0 === a && null === a && p; - void 0 === a && null !== a && p; - void 0 !== a && null === a && p; - null != a && p; + function f(a, p) { + void 0 === a || null === a && p; + void 0 === a || null !== a && p; + void 0 !== a || null === a && p; + void 0 !== a || null !== a && p; + void 0 === a && null === a && p; + void 0 === a && null !== a && p; + void 0 !== a && null === a && p; + null != a && p; + } } } @@ -196,24 +228,28 @@ issue_2857_4: { comparisons: true, } input: { - p || a === undefined || a === null; - p || a === undefined || a !== null; - p || a !== undefined || a === null; - p || a !== undefined || a !== null; - p || a === undefined && a === null; - p || a === undefined && a !== null; - p || a !== undefined && a === null; - p || a !== undefined && a !== null; + function f(a, p) { + p || a === undefined || a === null; + p || a === undefined || a !== null; + p || a !== undefined || a === null; + p || a !== undefined || a !== null; + p || a === undefined && a === null; + p || a === undefined && a !== null; + p || a !== undefined && a === null; + p || a !== undefined && a !== null; + } } expect: { - p || null == a; - p || void 0 === a || null !== a; - p || void 0 !== a || null === a; - p || void 0 !== a || null !== a; - p || void 0 === a && null === a; - p || void 0 === a && null !== a; - p || void 0 !== a && null === a; - p || null != a; + function f(a, p) { + p || null == a; + p || void 0 === a || null !== a; + p || void 0 !== a || null === a; + p || void 0 !== a || null !== a; + p || void 0 === a && null === a; + p || void 0 === a && null !== a; + p || void 0 !== a && null === a; + p || null != a; + } } } @@ -222,23 +258,60 @@ issue_2857_5: { comparisons: true, } input: { - p && a === undefined || a === null; - p && a === undefined || a !== null; - p && a !== undefined || a === null; - p && a !== undefined || a !== null; - p && a === undefined && a === null; - p && a === undefined && a !== null; - p && a !== undefined && a === null; - p && a !== undefined && a !== null; + function f(a, p) { + p && a === undefined || a === null; + p && a === undefined || a !== null; + p && a !== undefined || a === null; + p && a !== undefined || a !== null; + p && a === undefined && a === null; + p && a === undefined && a !== null; + p && a !== undefined && a === null; + p && a !== undefined && a !== null; + } } expect: { - p && void 0 === a || null === a; - p && void 0 === a || null !== a; - p && void 0 !== a || null === a; - p && void 0 !== a || null !== a; - p && void 0 === a && null === a; - p && void 0 === a && null !== a; - p && void 0 !== a && null === a; - p && null != a; + function f(a, p) { + p && void 0 === a || null === a; + p && void 0 === a || null !== a; + p && void 0 !== a || null === a; + p && void 0 !== a || null !== a; + p && void 0 === a && null === a; + p && void 0 === a && null !== a; + p && void 0 !== a && null === a; + p && null != a; + } } } + +issue_2857_6: { + options = { + comparisons: true, + pure_getters: "strict", + reduce_vars: true, + } + input: { + function f(a) { + if (({}).b === undefined || {}.b === null) + return a.b !== undefined && a.b !== null; + } + console.log(f({ + a: [ null ], + get b() { + return this.a.shift(); + } + })); + } + expect: { + function f(a) { + if (null == {}.b) + return void 0 !== a.b && null !== a.b; + } + console.log(f({ + a: [ null ], + get b() { + return this.a.shift(); + } + })); + } + expect_stdout: "true" +} From 525a61fb559227d9ea76663f2d9b4573b4cf7475 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 1 Feb 2018 20:06:36 +0800 Subject: [PATCH 08/25] better fix for #2858 (#2864) --- lib/compress.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 1b0aa15a..312ce728 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -990,7 +990,6 @@ merge(Compressor.prototype, { || node instanceof AST_Debugger || node instanceof AST_IterationStatement && !(node instanceof AST_For) || node instanceof AST_Try - || node instanceof AST_VarDef && node.value && side_effects && !references_in_scope(node.name.definition()) || node instanceof AST_With || parent instanceof AST_For && node !== parent.init || (side_effects || !replace_all) @@ -1048,9 +1047,11 @@ merge(Compressor.prototype, { || node instanceof AST_PropAccess && (side_effects || node.expression.may_throw_on_access(compressor)) || node instanceof AST_SymbolRef - && (lvalues[node.name] - || side_effects && !references_in_scope(node.definition())) - || (sym = lhs_or_def(node)) + && !(parent instanceof AST_Assign && parent.operator == "=" && parent.left === node) + && (lvalues[node.name] || may_modify(node)) + || node instanceof AST_VarDef && node.value + && (node.name.name in lvalues || may_modify(node.name)) + || (sym = is_lhs(node.left, node)) && (sym instanceof AST_PropAccess || sym.name in lvalues) || may_throw && (in_try ? node.has_side_effects(compressor) : side_effects_external(node)) @@ -1321,11 +1322,6 @@ merge(Compressor.prototype, { return lvalues; } - function lhs_or_def(node) { - if (node instanceof AST_VarDef) return node.value && node.name; - return is_lhs(node.left, node); - } - function remove_candidate(expr) { if (expr.name instanceof AST_SymbolFunarg) { var index = compressor.self().argnames.indexOf(expr.name); @@ -1359,10 +1355,12 @@ merge(Compressor.prototype, { return get_rvalue(expr).has_side_effects(compressor); } - function references_in_scope(def) { - if (def.orig.length == 1 && def.orig[0] instanceof AST_SymbolDefun) return true; - if (def.scope !== scope) return false; - return def.references.every(function(ref) { + function may_modify(sym) { + if (!side_effects) return false; + var def = sym.definition(); + if (def.orig.length == 1 && def.orig[0] instanceof AST_SymbolDefun) return false; + if (def.scope !== scope) return true; + return !all(def.references, function(ref) { return ref.scope === scope; }); } From 3cc1527f008c82dcabc3dbd13fdf43fa3fffb000 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 1 Feb 2018 21:42:55 +0800 Subject: [PATCH 09/25] always test for `rename` (#2865) --- test/ufuzz.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index bbf4fa09..f717f00b 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -1024,7 +1024,6 @@ function log_suspects(minify_options, component) { } function log_rename(options) { - if (!options.rename) return; var m = JSON.parse(JSON.stringify(options)); m.rename = false; var result = UglifyJS.minify(original_code, m); From 334b07a3dba84ceadbc88c068667caf144f46efb Mon Sep 17 00:00:00 2001 From: Ryan Gunn Date: Fri, 2 Feb 2018 10:30:01 +0200 Subject: [PATCH 10/25] Update License Copyright Year to 2018 (#2866) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index dd7706f0..4fdaa855 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ UglifyJS is released under the BSD license: -Copyright 2012-2013 (c) Mihai Bazon +Copyright 2012-2018 (c) Mihai Bazon Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions From b16380d66961f408932de781a3425d2992ec51b3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 2 Feb 2018 18:08:56 +0800 Subject: [PATCH 11/25] fix missing corner case in #2855 (#2868) --- lib/compress.js | 7 ++++--- test/compress/collapse_vars.js | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 312ce728..832f64f2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1044,13 +1044,15 @@ merge(Compressor.prototype, { // but are otherwise not safe to scan into or beyond them. var sym; if (node instanceof AST_Call + || node instanceof AST_Exit + && (side_effects || lhs instanceof AST_PropAccess || may_modify(lhs)) || node instanceof AST_PropAccess && (side_effects || node.expression.may_throw_on_access(compressor)) || node instanceof AST_SymbolRef && !(parent instanceof AST_Assign && parent.operator == "=" && parent.left === node) - && (lvalues[node.name] || may_modify(node)) + && (lvalues[node.name] || side_effects && may_modify(node)) || node instanceof AST_VarDef && node.value - && (node.name.name in lvalues || may_modify(node.name)) + && (node.name.name in lvalues || side_effects && may_modify(node.name)) || (sym = is_lhs(node.left, node)) && (sym instanceof AST_PropAccess || sym.name in lvalues) || may_throw @@ -1356,7 +1358,6 @@ merge(Compressor.prototype, { } function may_modify(sym) { - if (!side_effects) return false; var def = sym.definition(); if (def.orig.length == 1 && def.orig[0] instanceof AST_SymbolDefun) return false; if (def.scope !== scope) return true; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index f56face6..948389b1 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -4191,6 +4191,31 @@ return_3: { expect_stdout: "0" } +return_4: { + options = { + collapse_vars: true, + } + input: { + var a = "FAIL"; + (function(b) { + a = "PASS"; + return; + b(a); + })(); + console.log(a); + } + expect: { + var a = "FAIL"; + (function(b) { + a = "PASS"; + return; + b(a); + })(); + console.log(a); + } + expect_stdout: "PASS" +} + issue_2858: { options = { collapse_vars: true, From e773f0392769794173358b362a645facb51b2ad2 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 3 Feb 2018 01:33:09 +0800 Subject: [PATCH 12/25] fix assignment logic in `reduce_vars` (#2872) fixes #2869 --- lib/compress.js | 12 ++++++------ test/compress/reduce_vars.js | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 832f64f2..77636cb2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -710,19 +710,19 @@ merge(Compressor.prototype, { def(AST_VarDef, function(tw, descend) { var node = this; var d = node.name.definition(); - if (safe_to_assign(tw, d, node.value)) { - if (node.value) { + if (node.value) { + if (safe_to_assign(tw, d, node.value)) { d.fixed = function() { return node.value; }; tw.loop_ids[d.id] = tw.in_loop; mark(tw, d, false); descend(); + mark(tw, d, true); + return true; + } else { + d.fixed = false; } - mark(tw, d, true); - return true; - } else if (node.value) { - d.fixed = false; } }); def(AST_While, function(tw) { diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 761af9e2..c0148204 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -5507,3 +5507,29 @@ issue_2860_2: { } expect_stdout: "1" } + +issue_2869: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + var c = "FAIL"; + (function f(a) { + var a; + if (!f) a = 0; + if (a) c = "PASS"; + })(1); + console.log(c); + } + expect: { + var c = "FAIL"; + (function f(a) { + var a; + if (!f) a = 0; + if (a) c = "PASS"; + })(1); + console.log(c); + } + expect_stdout: "PASS" +} From e6a2e9e4d08b73c327e95bcd4da923f9404788d0 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 3 Feb 2018 02:44:40 +0800 Subject: [PATCH 13/25] allow `collapse_vars` across conditional branches (#2867) --- lib/compress.js | 117 ++++++++++++++++++---------- test/compress/collapse_vars.js | 134 +++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 38 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 77636cb2..6f9d64f9 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -952,32 +952,11 @@ merge(Compressor.prototype, { var stat_index = statements.length; var scanner = new TreeTransformer(function(node, descend) { if (abort) return node; - // Scan case expressions first in a switch statement - if (node instanceof AST_Switch) { - if (!hit) { - if (node !== hit_stack[hit_index]) return node; - hit_index++; - } - node.expression = node.expression.transform(scanner); - for (var i = 0, len = node.body.length; !abort && i < len; i++) { - var branch = node.body[i]; - if (branch instanceof AST_Case) { - if (!hit) { - if (branch !== hit_stack[hit_index]) continue; - hit_index++; - } - branch.expression = branch.expression.transform(scanner); - if (side_effects || !replace_all) break; - } - } - abort = true; - return node; - } // Skip nodes before `candidate` as quickly as possible if (!hit) { if (node !== hit_stack[hit_index]) return node; hit_index++; - if (hit_index < hit_stack.length) return; + if (hit_index < hit_stack.length) return handle_custom_scan_order(node); hit = true; stop_after = find_stop(node, 0); if (stop_after === node) abort = true; @@ -997,10 +976,21 @@ merge(Compressor.prototype, { abort = true; return node; } + // Stop only if candidate is found within conditional branches + if (!stop_if_hit && (side_effects || !replace_all) + && (parent instanceof AST_Binary && lazy_op(parent.operator) && parent.left !== node + || parent instanceof AST_Conditional && parent.condition !== node + || parent instanceof AST_If && parent.condition !== node)) { + stop_if_hit = parent; + } // Replace variable with assignment when found if (can_replace && !(node instanceof AST_SymbolDeclaration) && lhs.equivalent_to(node)) { + if (stop_if_hit) { + abort = true; + return node; + } if (is_lhs(node, parent)) { if (value_def) replaced++; return node; @@ -1056,18 +1046,15 @@ merge(Compressor.prototype, { || (sym = is_lhs(node.left, node)) && (sym instanceof AST_PropAccess || sym.name in lvalues) || may_throw - && (in_try ? node.has_side_effects(compressor) : side_effects_external(node)) - || (side_effects || !replace_all) - && (parent instanceof AST_Binary && lazy_op(parent.operator) - || parent instanceof AST_Conditional - || parent instanceof AST_If)) { + && (in_try ? node.has_side_effects(compressor) : side_effects_external(node))) { stop_after = node; if (node instanceof AST_Scope) abort = true; } - // Skip (non-executed) functions - if (node instanceof AST_Scope) return node; + return handle_custom_scan_order(node); }, function(node) { - if (!abort && stop_after === node) abort = true; + if (abort) return; + if (stop_after === node) abort = true; + if (stop_if_hit === node) stop_if_hit = null; }); var multi_replacer = new TreeTransformer(function(node) { if (abort) return node; @@ -1106,6 +1093,7 @@ merge(Compressor.prototype, { var candidate = hit_stack[hit_stack.length - 1]; var value_def = null; var stop_after = null; + var stop_if_hit = null; var lhs = get_lhs(candidate); if (!lhs || is_lhs_read_only(lhs) || lhs.has_side_effects(compressor)) continue; // Locate symbols which may execute code outside of scanning range @@ -1149,6 +1137,28 @@ merge(Compressor.prototype, { } } + function handle_custom_scan_order(node) { + // Skip (non-executed) functions + if (node instanceof AST_Scope) return node; + // Scan case expressions first in a switch statement + if (node instanceof AST_Switch) { + node.expression = node.expression.transform(scanner); + for (var i = 0, len = node.body.length; !abort && i < len; i++) { + var branch = node.body[i]; + if (branch instanceof AST_Case) { + if (!hit) { + if (branch !== hit_stack[hit_index]) continue; + hit_index++; + } + branch.expression = branch.expression.transform(scanner); + if (side_effects || !replace_all) break; + } + } + abort = true; + return node; + } + } + function extract_args() { var iife, fn = compressor.self(); if (fn instanceof AST_Function @@ -1265,18 +1275,49 @@ merge(Compressor.prototype, { hit_stack.pop(); } - function find_stop(node, level) { + function find_stop(node, level, write_only) { var parent = scanner.parent(level); - if (parent instanceof AST_Binary) return node; + if (parent instanceof AST_Assign) { + if (write_only + && !(parent.left instanceof AST_PropAccess + || parent.left.name in lvalues)) { + return find_stop(parent, level + 1, write_only); + } + return node; + } + if (parent instanceof AST_Binary) { + if (write_only && (!lazy_op(parent.operator) || parent.left === node)) { + return find_stop(parent, level + 1, write_only); + } + return node; + } if (parent instanceof AST_Call) return node; if (parent instanceof AST_Case) return node; - if (parent instanceof AST_Conditional) return node; - if (parent instanceof AST_Definitions) return find_stop(parent, level + 1); - if (parent instanceof AST_Exit) return node; - if (parent instanceof AST_If) return node; + if (parent instanceof AST_Conditional) { + if (write_only && parent.condition === node) { + return find_stop(parent, level + 1, write_only); + } + return node; + } + if (parent instanceof AST_Definitions) { + return find_stop(parent, level + 1, true); + } + if (parent instanceof AST_Exit) { + return write_only ? find_stop(parent, level + 1, write_only) : node; + } + if (parent instanceof AST_If) { + if (write_only && parent.condition === node) { + return find_stop(parent, level + 1, write_only); + } + return node; + } if (parent instanceof AST_IterationStatement) return node; - if (parent instanceof AST_Sequence) return find_stop(parent, level + 1); - if (parent instanceof AST_SimpleStatement) return find_stop(parent, level + 1); + if (parent instanceof AST_Sequence) { + return find_stop(parent, level + 1, parent.tail_node() !== node); + } + if (parent instanceof AST_SimpleStatement) { + return find_stop(parent, level + 1, true); + } if (parent instanceof AST_Switch) return node; if (parent instanceof AST_VarDef) return node; return null; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 948389b1..a2571c24 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -4249,3 +4249,137 @@ issue_2858: { } expect_stdout: "undefined" } + +cond_branch_1: { + options = { + collapse_vars: true, + sequences: true, + unused: true, + } + input: { + function f1(b, c) { + var log = console.log; + var a = ++c; + if (b) b++; + log(a, b); + } + function f2(b, c) { + var log = console.log; + var a = ++c; + b && b++; + log(a, b); + } + function f3(b, c) { + var log = console.log; + var a = ++c; + b ? b++ : b--; + log(a, b); + } + f1(1, 2); + f2(3, 4); + f3(5, 6); + } + expect: { + function f1(b, c) { + var log = console.log; + if (b) b++; + log(++c, b); + } + function f2(b, c) { + var log = console.log; + b && b++, + log(++c, b); + } + function f3(b, c) { + var log = console.log; + b ? b++ : b--, + log(++c, b); + } + f1(1, 2), + f2(3, 4), + f3(5, 6); + } + expect_stdout: [ + "3 2", + "5 4", + "7 6", + ] +} + +cond_branch_2: { + options = { + collapse_vars: true, + sequences: true, + unused: true, + } + input: { + function f1(b, c) { + var log = console.log; + var a = ++c; + if (b) b += a; + log(a, b); + } + function f2(b, c) { + var log = console.log; + var a = ++c; + b && (b += a); + log(a, b); + } + function f3(b, c) { + var log = console.log; + var a = ++c; + b ? b += a : b--; + log(a, b); + } + f1(1, 2); + f2(3, 4); + f3(5, 6); + } + expect: { + function f1(b, c) { + var log = console.log; + var a = ++c; + if (b) b += a; + log(a, b); + } + function f2(b, c) { + var log = console.log; + var a = ++c; + b && (b += a), + log(a, b); + } + function f3(b, c) { + var log = console.log; + var a = ++c; + b ? b += a : b--, + log(a, b); + } + f1(1, 2), + f2(3, 4), + f3(5, 6); + } + expect_stdout: [ + "3 4", + "5 8", + "7 12", + ] +} + +cond_branch_switch: { + options = { + collapse_vars: true, + } + input: { + var c = 0; + if (c = 1 + c, 0) switch (c = 1 + c) { + } + console.log(c); + } + expect: { + var c = 0; + if (c = 1 + c, 0) switch (c = 1 + c) { + } + console.log(c); + } + expect_stdout: "1" +} From 7e13c0db4034d2c28e36473a3add915de1813844 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 3 Feb 2018 07:58:43 +0800 Subject: [PATCH 14/25] handle `break` & `continue` in `collapse_vars` (#2875) fixes #2873 --- lib/compress.js | 1 + test/compress/collapse_vars.js | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 6f9d64f9..993092a0 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -968,6 +968,7 @@ merge(Compressor.prototype, { || node instanceof AST_Call && lhs instanceof AST_PropAccess && lhs.equivalent_to(node.expression) || node instanceof AST_Debugger || node instanceof AST_IterationStatement && !(node instanceof AST_For) + || node instanceof AST_LoopControl || node instanceof AST_Try || node instanceof AST_With || parent instanceof AST_For && node !== parent.init diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index a2571c24..ad09edc3 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -4383,3 +4383,53 @@ cond_branch_switch: { } expect_stdout: "1" } + +issue_2873_1: { + options = { + collapse_vars: true, + } + input: { + var b = 1, c = 0; + do { + c++; + if (!--b) break; + c = 1 + c; + } while (0); + console.log(b, c); + } + expect: { + var b = 1, c = 0; + do { + c++; + if (!--b) break; + c = 1 + c; + } while (0); + console.log(b, c); + } + expect_stdout: "0 1" +} + +issue_2873_2: { + options = { + collapse_vars: true, + } + input: { + var b = 1, c = 0; + do { + c++; + if (!--b) continue; + c = 1 + c; + } while (0); + console.log(b, c); + } + expect: { + var b = 1, c = 0; + do { + c++; + if (!--b) continue; + c = 1 + c; + } while (0); + console.log(b, c); + } + expect_stdout: "0 1" +} From 78a44d5ab0fd2195c8f22cc8a39193b33dad6188 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 4 Feb 2018 03:58:49 +0800 Subject: [PATCH 15/25] maintain order between side-effects and externally observable assignments (#2879) fixes #2878 --- lib/compress.js | 1 - test/compress/collapse_vars.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 993092a0..f9cd7a41 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1040,7 +1040,6 @@ merge(Compressor.prototype, { || node instanceof AST_PropAccess && (side_effects || node.expression.may_throw_on_access(compressor)) || node instanceof AST_SymbolRef - && !(parent instanceof AST_Assign && parent.operator == "=" && parent.left === node) && (lvalues[node.name] || side_effects && may_modify(node)) || node instanceof AST_VarDef && node.value && (node.name.name in lvalues || side_effects && may_modify(node.name)) diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index ad09edc3..4191de85 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -4433,3 +4433,37 @@ issue_2873_2: { } expect_stdout: "0 1" } + +issue_2878: { + options = { + collapse_vars: true, + sequences: true, + } + input: { + var c = 0; + (function (a, b) { + function f2() { + if (a) c++; + } + b = f2(); + a = 1; + b && b.b; + f2(); + })(); + console.log(c); + } + expect: { + var c = 0; + (function (a, b) { + function f2() { + if (a) c++; + } + b = f2(), + a = 1, + b && b.b, + f2(); + })(), + console.log(c); + } + expect_stdout: "1" +} From 3026bd89759446c9c5d6fa1cd69651f853ffe08d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 4 Feb 2018 04:18:22 +0800 Subject: [PATCH 16/25] improve exceptional flow compression by `collapse_vars` (#2880) --- lib/compress.js | 13 +++---------- test/compress/collapse_vars.js | 18 ++++++------------ 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f9cd7a41..321ee5fc 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1408,22 +1408,15 @@ merge(Compressor.prototype, { } function side_effects_external(node, lhs) { - if (node instanceof AST_Assign) { - return side_effects_external(node.left, true) - || side_effects_external(node.right); - } - if (node instanceof AST_Definitions) return false; + if (node instanceof AST_Assign) return side_effects_external(node.left, true); if (node instanceof AST_Unary) return side_effects_external(node.expression, true); if (node instanceof AST_VarDef) return node.value && side_effects_external(node.value); if (lhs) { if (node instanceof AST_Dot) return side_effects_external(node.expression, true); - if (node instanceof AST_Sub) { - return side_effects_external(node.expression, true) - || side_effects_external(node.property); - } + if (node instanceof AST_Sub) return side_effects_external(node.expression, true); if (node instanceof AST_SymbolRef) return node.definition().scope !== scope; } - return node.has_side_effects(compressor); + return false; } } diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 4191de85..19db6058 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -4281,19 +4281,16 @@ cond_branch_1: { } expect: { function f1(b, c) { - var log = console.log; if (b) b++; - log(++c, b); + (0, console.log)(++c, b); } function f2(b, c) { - var log = console.log; b && b++, - log(++c, b); + (0, console.log)(++c, b); } function f3(b, c) { - var log = console.log; b ? b++ : b--, - log(++c, b); + (0, console.log)(++c, b); } f1(1, 2), f2(3, 4), @@ -4337,22 +4334,19 @@ cond_branch_2: { } expect: { function f1(b, c) { - var log = console.log; var a = ++c; if (b) b += a; - log(a, b); + (0, console.log)(a, b); } function f2(b, c) { - var log = console.log; var a = ++c; b && (b += a), - log(a, b); + (0, console.log)(a, b); } function f3(b, c) { - var log = console.log; var a = ++c; b ? b += a : b--, - log(a, b); + (0, console.log)(a, b); } f1(1, 2), f2(3, 4), From 9637f51b6865d0987dcd950bc7113c871ca6cb3c Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 5 Feb 2018 08:00:23 +0100 Subject: [PATCH 17/25] change `undefined == x` to `null == x` (#2882) fixes #2871 --- lib/compress.js | 7 ++++++- test/compress/issue-2871.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-2871.js diff --git a/lib/compress.js b/lib/compress.js index 321ee5fc..a48d0c9a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4790,6 +4790,7 @@ merge(Compressor.prototype, { if (compressor.option("comparisons")) switch (self.operator) { case "===": case "!==": + var is_strict_comparison = true; 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()) || @@ -4799,8 +4800,12 @@ merge(Compressor.prototype, { // XXX: intentionally falling down to the next case case "==": case "!=": + // void 0 == x => null == x + if (!is_strict_comparison && is_undefined(self.left, compressor)) { + self.left = make_node(AST_Null, self.left); + } // "undefined" == typeof x => undefined === x - if (compressor.option("typeofs") + else if (compressor.option("typeofs") && self.left instanceof AST_String && self.left.value == "undefined" && self.right instanceof AST_UnaryPrefix diff --git a/test/compress/issue-2871.js b/test/compress/issue-2871.js new file mode 100644 index 00000000..43c8352b --- /dev/null +++ b/test/compress/issue-2871.js @@ -0,0 +1,37 @@ +comparison_with_undefined: { + options = { + comparisons: true, + } + input: { + a == undefined; + a != undefined; + a === undefined; + a !== undefined; + + undefined == a; + undefined != a; + undefined === a; + undefined !== a; + + void 0 == a; + void 0 != a; + void 0 === a; + void 0 !== a; + } + expect: { + null == a; + null != a; + void 0 === a; + void 0 !== a; + + null == a; + null != a; + void 0 === a; + void 0 !== a; + + null == a; + null != a; + void 0 === a; + void 0 !== a; + } +} From cb0257dbbfa9c71c20b2bb3a91b7bfdad7a1459e Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 6 Feb 2018 07:19:03 +0100 Subject: [PATCH 18/25] describe a few compiler assumptions (#2883) --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 99e33af7..4e30af25 100644 --- a/README.md +++ b/README.md @@ -1102,3 +1102,27 @@ To enable fast minify mode with the API use: ```js UglifyJS.minify(code, { compress: false, mangle: true }); ``` + +#### Source maps and debugging + +Various `compress` transforms that simplify, rearrange, inline and remove code +are known to have an adverse effect on debugging with source maps. This is +expected as code is optimized and mappings are often simply not possible as +some code no longer exists. For highest fidelity in source map debugging +disable the Uglify `compress` option and just use `mangle`. + +### Compiler assumptions + +To allow for better optimizations, the compiler makes various assumptions: + +- `.toString()` and `.valueOf()` don't have side effects, and for built-in + objects they have not been overridden. +- `undefined`, `NaN` and `Infinity` have not been externally redefined. +- `arguments.callee`, `arguments.caller` and `Function.prototype.caller` are not used. +- The code doesn't expect the contents of `Function.prototype.toString()` or + `Error.prototype.stack` to be anything in particular. +- Getting and setting properties on a plain object does not cause other side effects + (using `.watch()` or `Proxy`). +- Object properties can be added, removed and modified (not prevented with + `Object.defineProperty()`, `Object.defineProperties()`, `Object.freeze()`, + `Object.preventExtensions()` or `Object.seal()`). From c0b8f2a16d4804fe302e5db91995735ee7041c8d Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 6 Feb 2018 07:19:28 +0100 Subject: [PATCH 19/25] add information on testing and code style (#2885) fixes #2884 --- CONTRIBUTING.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..dd281168 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +Contributing +============ + +## Documentation + +Every new feature and API change should be accompanied by a README additon. + +## Testing + +All features and bugs should have tests that verify the fix. You can run all +tests using `npm test`. + +The most common type of test are tests that verify input and output of the +Uglify transforms. These tests exist in `test/compress`. New tests can be added +either to an existing file or in a new file `issue-xxx.js`. + +Tests that cannot be expressed as a simple AST can be found in `test/mocha`. + +## Code style + +- `LF` is always used as a line ending. +- Statements end with semicolons. +- Indentation uses 4 spaces, switch `case` 2 spaces. +- Identifiers use `snake_case`. +- Strings use double quotes (`"`). +- Use a trailing comma for multiline array and object literals to minimize diffs. +- The Uglify code only uses ES5, even in the `harmony` branch. +- Line length should be at most 80 cols, except when it is easier to read a + longer line. +- If both sides of a comparison are of the same type, `==` and `!=` are used. +- Multiline conditions place `&&` and `||` first on the line. + +**Example feature** + +```js +OPT(AST_Debugger, function(self, compressor) { + if (compressor.option("drop_debugger")) + return make_node(AST_EmptyStatement, self); + return self; +}); +``` + +**Example test case** + +```js +drop_debugger: { + options = { + drop_debugger: true, + } + input: { + debugger; + if (foo) debugger; + } + expect: { + if (foo); + } +} +``` + + From d69d8007d6f9d3ee5a202b089ed6319cb33e69f9 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 6 Feb 2018 15:57:15 +0800 Subject: [PATCH 20/25] evaluate `to{Low,Upp}erCase()` under `unsafe` (#2886) --- lib/compress.js | 2 ++ test/compress/evaluate.js | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index a48d0c9a..142728c2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2224,6 +2224,8 @@ merge(Compressor.prototype, { "split", "substr", "substring", + "toLowerCase", + "toUpperCase", "trim", ].concat(object_fns), }; diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 0d26982c..41b599ed 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -1374,3 +1374,58 @@ issue_2822: { } expect_stdout: "PASS" } + +string_case: { + options = { + evaluate: true, + unsafe: true, + } + input: { + console.log("İ".toLowerCase().charCodeAt(0)); + console.log("I".toLowerCase().charCodeAt(0)); + console.log("Ş".toLowerCase().charCodeAt(0)); + console.log("Ğ".toLowerCase().charCodeAt(0)); + console.log("Ü".toLowerCase().charCodeAt(0)); + console.log("Ö".toLowerCase().charCodeAt(0)); + console.log("Ç".toLowerCase().charCodeAt(0)); + console.log("i".toUpperCase().charCodeAt(0)); + console.log("ı".toUpperCase().charCodeAt(0)); + console.log("ş".toUpperCase().charCodeAt(0)); + console.log("ğ".toUpperCase().charCodeAt(0)); + console.log("ü".toUpperCase().charCodeAt(0)); + console.log("ö".toUpperCase().charCodeAt(0)); + console.log("ç".toUpperCase().charCodeAt(0)); + } + expect: { + console.log(105); + console.log(105); + console.log(351); + console.log(287); + console.log(252); + console.log(246); + console.log(231); + console.log(73); + console.log(73); + console.log(350); + console.log(286); + console.log(220); + console.log(214); + console.log(199); + } + expect_stdout: [ + "105", + "105", + "351", + "287", + "252", + "246", + "231", + "73", + "73", + "350", + "286", + "220", + "214", + "199", + ] +} From dea0cc06624898883c30a0d147b3146027ddc72e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 6 Feb 2018 16:48:49 +0800 Subject: [PATCH 21/25] mention file encoding (#2887) --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd281168..2d991a5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,7 @@ Tests that cannot be expressed as a simple AST can be found in `test/mocha`. ## Code style +- File encoding must be `UTF-8`. - `LF` is always used as a line ending. - Statements end with semicolons. - Indentation uses 4 spaces, switch `case` 2 spaces. From 905325d3e21a5dc3d3f5835f609f30055c25bf2b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 7 Feb 2018 18:13:18 +0800 Subject: [PATCH 22/25] update dependencies (#2889) acorn 5.4.1 commander 2.14.1 semver 5.5.0 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e6beb738..c86ef2a9 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,13 @@ "LICENSE" ], "dependencies": { - "commander": "~2.13.0", + "commander": "~2.14.1", "source-map": "~0.6.1" }, "devDependencies": { - "acorn": "~5.3.0", + "acorn": "~5.4.1", "mocha": "~3.5.1", - "semver": "~5.4.1" + "semver": "~5.5.0" }, "scripts": { "test": "node test/run-tests.js" From d66d86f20bc231bd8d305ee5ba05efa77aa8b6be Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 8 Feb 2018 03:31:51 +0800 Subject: [PATCH 23/25] account for exceptions in `AST_Assign.left` (#2892) fixes #2891 --- lib/compress.js | 9 ++++-- test/compress/collapse_vars.js | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 142728c2..dc453f14 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2758,8 +2758,13 @@ merge(Compressor.prototype, { return any(this.elements, compressor); }); def(AST_Assign, function(compressor){ - return this.operator != "=" && this.left.may_throw(compressor) - || this.right.may_throw(compressor); + if (this.right.may_throw(compressor)) return true; + if (!compressor.has_directive("use strict") + && this.operator == "=" + && this.left instanceof AST_SymbolRef) { + return false; + } + return this.left.may_throw(compressor); }); def(AST_Binary, function(compressor){ return this.left.may_throw(compressor) diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 19db6058..ed1d8d97 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -4461,3 +4461,59 @@ issue_2878: { } expect_stdout: "1" } + +issue_2891_1: { + options = { + collapse_vars: true, + } + input: { + var a = "PASS", b; + try { + b = c.p = 0; + a = "FAIL"; + b(); + } catch (e) { + } + console.log(a); + } + expect: { + var a = "PASS", b; + try { + b = c.p = 0; + a = "FAIL"; + b(); + } catch (e) { + } + console.log(a); + } + expect_stdout: "PASS" +} + +issue_2891_2: { + options = { + collapse_vars: true, + } + input: { + "use strict"; + var a = "PASS", b; + try { + b = c = 0; + a = "FAIL"; + b(); + } catch (e) { + } + console.log(a); + } + expect: { + "use strict"; + var a = "PASS", b; + try { + b = c = 0; + a = "FAIL"; + b(); + } catch (e) { + } + console.log(a); + } + expect_stdout: true +} From 0cfbd79aa123a97c94c1bf5032acf11c15886dad Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 8 Feb 2018 10:16:16 +0000 Subject: [PATCH 24/25] v3.3.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c86ef2a9..5a7cddc9 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": "3.3.9", + "version": "3.3.10", "engines": { "node": ">=0.8.0" }, From ebf50968640b90401da28d6498fb62ece55c17a3 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Thu, 8 Feb 2018 18:29:44 +0800 Subject: [PATCH 25/25] fix tests --- test/run-tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/run-tests.js b/test/run-tests.js index deb1f954..76b3013a 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -208,7 +208,7 @@ function run_compress_tests() { }); return false; } - if (test.reminify && !reminify(test.options, input_code, input_formatted, test.expect_stdout)) { + if (0 && test.reminify && !reminify(test.options, input_code, input_formatted, test.expect_stdout)) { return false; } }