From 0e16d92786b8360848a4b56719135facabe7cd85 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 11 Dec 2017 00:16:02 +0800 Subject: [PATCH 01/42] handle exceptional flow correctly in `collapse_vars` (#2574) fixes #2571 --- lib/compress.js | 151 +++++++++++++++++++++++++++++---- test/compress/collapse_vars.js | 84 ++++++++++++++---- 2 files changed, 203 insertions(+), 32 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index ce0fbdd5..434becb6 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -868,6 +868,21 @@ 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) { + 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) + branch.expression = branch.expression.transform(scanner); + } + for (i = 0; !abort && i < len; i++) { + var branch = node.body[i]; + for (var j = 0, len2 = branch.body.length; j < len2; j++) + branch.body[j] = branch.body[j].transform(scanner); + } + return node; + } // Skip nodes before `candidate` as quickly as possible if (!hit) { if (node === candidate) { @@ -944,17 +959,17 @@ merge(Compressor.prototype, { || side_effects && !references_in_scope(node.definition())) || (sym = lhs_or_def(node)) && (sym instanceof AST_PropAccess || sym.name in lvalues) + || may_throw && node.has_side_effects(compressor) || (side_effects || !replace_all) && (parent instanceof AST_Binary && lazy_op(parent.operator) - || parent instanceof AST_Case || parent instanceof AST_Conditional || parent instanceof AST_If)) { if (!(node instanceof AST_Scope)) descend(node, scanner); abort = true; return node; } - // Skip (non-executed) functions and (leading) default case in switch statements - if (node instanceof AST_Default || node instanceof AST_Scope) return node; + // Skip (non-executed) functions + if (node instanceof AST_Scope) return node; }); var multi_replacer = new TreeTransformer(function(node) { if (abort) return node; @@ -1000,6 +1015,7 @@ merge(Compressor.prototype, { replace_all = def.references.length - def.replaced == 1; } var side_effects = value_has_side_effects(candidate); + var may_throw = candidate.may_throw(compressor); var funarg = candidate.name instanceof AST_SymbolFunarg; var hit = funarg; var abort = false, replaced = 0, can_replace = !args || !hit; @@ -2200,15 +2216,6 @@ merge(Compressor.prototype, { def(AST_Constant, return_false); def(AST_This, return_false); - def(AST_Call, function(compressor){ - if (!this.is_expr_pure(compressor)) return true; - for (var i = this.args.length; --i >= 0;) { - if (this.args[i].has_side_effects(compressor)) - return true; - } - return false; - }); - function any(list, compressor) { for (var i = list.length; --i >= 0;) if (list[i].has_side_effects(compressor)) @@ -2219,6 +2226,10 @@ merge(Compressor.prototype, { def(AST_Block, function(compressor){ return any(this.body, compressor); }); + def(AST_Call, function(compressor){ + return !this.is_expr_pure(compressor) + || any(this.args, compressor); + }); def(AST_Switch, function(compressor){ return this.expression.has_side_effects(compressor) || any(this.body, compressor); @@ -2243,8 +2254,7 @@ merge(Compressor.prototype, { def(AST_SimpleStatement, function(compressor){ return this.body.has_side_effects(compressor); }); - def(AST_Defun, return_true); - def(AST_Function, return_false); + def(AST_Lambda, return_false); def(AST_Binary, function(compressor){ return this.left.has_side_effects(compressor) || this.right.has_side_effects(compressor); @@ -2282,14 +2292,121 @@ merge(Compressor.prototype, { || this.property.has_side_effects(compressor); }); def(AST_Sequence, function(compressor){ - return this.expressions.some(function(expression, index) { - return expression.has_side_effects(compressor); - }); + return any(this.expressions, compressor); + }); + def(AST_Definitions, function(compressor){ + return any(this.definitions, compressor); + }); + def(AST_VarDef, function(compressor){ + return this.value; }); })(function(node, func){ node.DEFMETHOD("has_side_effects", func); }); + // determine if expression may throw + (function(def){ + def(AST_Node, return_true); + + def(AST_Constant, return_false); + def(AST_EmptyStatement, return_false); + def(AST_Lambda, return_false); + def(AST_SymbolDeclaration, return_false); + def(AST_This, return_false); + + function any(list, compressor) { + for (var i = list.length; --i >= 0;) + if (list[i].may_throw(compressor)) + return true; + return false; + } + + def(AST_Array, function(compressor){ + return any(this.elements, compressor); + }); + def(AST_Assign, function(compressor){ + return this.operator != "=" && this.left.may_throw(compressor) + || this.right.may_throw(compressor); + }); + def(AST_Binary, function(compressor){ + return this.left.may_throw(compressor) + || this.right.may_throw(compressor); + }); + def(AST_Block, function(compressor){ + return any(this.body, compressor); + }); + def(AST_Call, function(compressor){ + if (any(this.args, compressor)) return true; + if (this.is_expr_pure(compressor)) return false; + if (this.expression.may_throw(compressor)) return true; + return !(this.expression instanceof AST_Lambda) + || any(this.expression.body, compressor); + }); + def(AST_Case, function(compressor){ + return this.expression.may_throw(compressor) + || any(this.body, compressor); + }); + def(AST_Conditional, function(compressor){ + return this.condition.may_throw(compressor) + || this.consequent.may_throw(compressor) + || this.alternative.may_throw(compressor); + }); + def(AST_Definitions, function(compressor){ + return any(this.definitions, compressor); + }); + def(AST_Dot, function(compressor){ + return this.expression.may_throw_on_access(compressor) + || this.expression.may_throw(compressor); + }); + def(AST_If, function(compressor){ + return this.condition.may_throw(compressor) + || this.body && this.body.may_throw(compressor) + || this.alternative && this.alternative.may_throw(compressor); + }); + def(AST_LabeledStatement, function(compressor){ + return this.body.may_throw(compressor); + }); + def(AST_Object, function(compressor){ + return any(this.properties, compressor); + }); + def(AST_ObjectProperty, function(compressor){ + return this.value.may_throw(compressor); + }); + def(AST_Sequence, function(compressor){ + return any(this.expressions, compressor); + }); + def(AST_SimpleStatement, function(compressor){ + return this.body.may_throw(compressor); + }); + def(AST_Sub, function(compressor){ + return this.expression.may_throw_on_access(compressor) + || this.expression.may_throw(compressor) + || this.property.may_throw(compressor); + }); + def(AST_Switch, function(compressor){ + return this.expression.may_throw(compressor) + || any(this.body, compressor); + }); + def(AST_SymbolRef, function(compressor){ + return !this.is_declared(compressor); + }); + def(AST_Try, function(compressor){ + return any(this.body, compressor) + || this.bcatch && this.bcatch.may_throw(compressor) + || this.bfinally && this.bfinally.may_throw(compressor); + }); + def(AST_Unary, function(compressor){ + if (this.operator == "typeof" && this.expression instanceof AST_SymbolRef) + return false; + return this.expression.may_throw(compressor); + }); + def(AST_VarDef, function(compressor){ + return this.value.may_throw(compressor); + }); + })(function(node, func){ + node.DEFMETHOD("may_throw", func); + }); + // determine if expression is constant (function(def){ function all(list) { diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 68313354..ab86c6b4 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -69,10 +69,11 @@ collapse_vars_side_effects_1: { log(x, s.charAt(i++), y, 7); } function f4() { - var i = 10, + var log = console.log.bind(console), + i = 10, x = i += 2, y = i += 3; - console.log.bind(console)(x, i += 4, y, i); + log(x, i += 4, y, i); } f1(), f2(), f3(), f4(); } @@ -152,7 +153,7 @@ collapse_vars_issue_721: { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, - reduce_funcs: true, reduce_vars:true + reduce_funcs: true, reduce_vars:true, passes:2 } input: { define(["require", "exports", 'handlebars'], function (require, exports, hb) { @@ -675,8 +676,8 @@ collapse_vars_lvalues: { function f4(x) { var a = (x -= 3); return x + a; } function f5(x) { var w = e1(), v = e2(), c = v = --x; return (w = x) - c; } function f6(x) { var w = e1(), v = e2(); return (v = --x) - (w = x); } - function f7(x) { var w = e1(); return (w = x) - (e2() - x); } - function f8(x) { var w = e1(); return (w = x) - (e2() - x); } + function f7(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } + function f8(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } function f9(x) { var w = e1(); return e2() - x - (w = x); } } } @@ -685,7 +686,7 @@ collapse_vars_lvalues_drop_assign: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, passes:3 } input: { function f0(x) { var i = ++x; return x += i; } @@ -706,10 +707,10 @@ collapse_vars_lvalues_drop_assign: { function f3(x) { var a = (x -= 3); return x + a; } function f4(x) { var a = (x -= 3); return x + a; } function f5(x) { e1(), e2(); var c = --x; return x - c; } - function f6(x) { e1(), e2(); return --x - x; } - function f7(x) { e1(); return x - (e2() - x); } - function f8(x) { e1(); return x - (e2() - x); } - function f9(x) { e1(); return e2() - x - x; } + function f6(x) { return e1(), e2(), --x - x; } + function f7(x) { return e1(), x - (e2() - x); } + function f8(x) { return e1(), x - (e2() - x); } + function f9(x) { return e1(), e2() - x - x; } } } @@ -1767,11 +1768,10 @@ switch_case: { } expect: { function f(x, y, z) { - var c = z; switch (x()) { default: d(); case y(): e(); - case c: f(); + case z: f(); } } } @@ -2195,7 +2195,7 @@ issue_315: { expect: { console.log(function() { var w, _i, _len, _ref, _results; - for (_results = [], _i = 0, _len = (_ref = "test".trim().split(" ")).length; _i < _len ; _i++) + for (_ref = "test".trim().split(" "), _results = [], _i = 0, _len = _ref.length; _i < _len ; _i++) w = _ref[_i], _results.push(w.toLowerCase()); return _results; }()); @@ -3108,8 +3108,8 @@ issue_2437: { return Object.defineProperty(XMLHttpRequest.prototype, "onreadystatechange", xhrDesc || {}), result; } - var req, detectFunc = function() {}; - (req = new XMLHttpRequest()).onreadystatechange = detectFunc; + var req = new XMLHttpRequest(), detectFunc = function() {}; + req.onreadystatechange = detectFunc; result = req[SYMBOL_FAKE_ONREADYSTATECHANGE_1] === detectFunc; req.onreadystatechange = null; }(); @@ -3675,3 +3675,57 @@ issue_2506: { } expect_stdout: "1" } + +issue_2571_1: { + options = { + collapse_vars: true, + toplevel: true, + } + input: { + var b = 1; + try { + var a = function f0(c) { + throw c; + }(2); + var d = --b + a; + } catch (e) { + } + console.log(b); + } + expect: { + var b = 1; + try { + var a = function f0(c) { + throw c; + }(2); + var d = --b + a; + } catch (e) { + } + console.log(b); + } + expect_stdout: "1" +} + +issue_2571_2: { + options = { + collapse_vars: true, + toplevel: true, + } + input: { + try { + var a = A, b = 1; + throw a; + } catch (e) { + console.log(b); + } + } + expect: { + try { + var a = A, b = 1; + throw a; + } catch (e) { + console.log(b); + } + } + expect_stdout: "undefined" +} From bf000beae710a2b4129bddb009bb706ff036fbb6 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 11 Dec 2017 00:24:54 +0800 Subject: [PATCH 02/42] rename tests (#2575) --- test/compress/reduce_vars.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index df2eb712..6c8bd639 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -4545,7 +4545,7 @@ issue_2455: { } } -issue_2560_1: { +escape_conditional: { options = { reduce_funcs: true, reduce_vars: true, @@ -4584,7 +4584,7 @@ issue_2560_1: { expect_stdout: "PASS" } -issue_2560_2: { +escape_sequence: { options = { reduce_funcs: true, reduce_vars: true, @@ -4622,7 +4622,7 @@ issue_2560_2: { expect_stdout: "PASS" } -issue_2560_3: { +escape_throw: { options = { reduce_funcs: true, reduce_vars: true, @@ -4667,7 +4667,7 @@ issue_2560_3: { expect_stdout: "PASS" } -issue_2560_4: { +escape_local_conditional: { options = { reduce_funcs: true, reduce_vars: true, @@ -4704,7 +4704,7 @@ issue_2560_4: { expect_stdout: "PASS" } -issue_2560_5: { +escape_local_sequence: { options = { reduce_funcs: true, reduce_vars: true, @@ -4741,7 +4741,7 @@ issue_2560_5: { expect_stdout: "PASS" } -issue_2560_6: { +escape_local_throw: { options = { reduce_funcs: true, reduce_vars: true, From 93f3b2b114877af17db219e501ae4551df61738d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 11 Dec 2017 01:15:44 +0800 Subject: [PATCH 03/42] escape consecutive unpaired surrogates (#2576) fixes #2569 --- lib/output.js | 15 ++++++++++----- lib/parse.js | 12 ++++++++++++ test/compress/unicode.js | 7 +++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/output.js b/lib/output.js index 1aa63450..a4c41f11 100644 --- a/lib/output.js +++ b/lib/output.js @@ -121,11 +121,16 @@ function OutputStream(options) { } }); } : function(str) { - return str.replace(/[\ud800-\udbff](?![\udc00-\udfff])/g, function(ch) { - return "\\u" + ch.charCodeAt(0).toString(16); - }).replace(/(^|[^\ud800-\udbff])([\udc00-\udfff])/g, function(match, prefix, ch) { - return prefix + "\\u" + ch.charCodeAt(0).toString(16); - }); + var s = ""; + for (var i = 0, len = str.length; i < len; i++) { + if (is_surrogate_pair_head(str[i]) && !is_surrogate_pair_tail(str[i + 1]) + || is_surrogate_pair_tail(str[i]) && !is_surrogate_pair_head(str[i - 1])) { + s += "\\u" + str.charCodeAt(i).toString(16); + } else { + s += str[i]; + } + } + return s; }; function make_string(str, quote) { diff --git a/lib/parse.js b/lib/parse.js index 099fc49a..f0098c75 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -132,6 +132,18 @@ function is_letter(code) { || (code >= 0xaa && UNICODE.letter.test(String.fromCharCode(code))); }; +function is_surrogate_pair_head(code) { + if (typeof code == "string") + code = code.charCodeAt(0); + return code >= 0xd800 && code <= 0xdbff; +} + +function is_surrogate_pair_tail(code) { + if (typeof code == "string") + code = code.charCodeAt(0); + return code >= 0xdc00 && code <= 0xdfff; +} + function is_digit(code) { return code >= 48 && code <= 57; }; diff --git a/test/compress/unicode.js b/test/compress/unicode.js index 4dbc197c..c4bd5de8 100644 --- a/test/compress/unicode.js +++ b/test/compress/unicode.js @@ -55,3 +55,10 @@ issue_2242_4: { } expect_exact: 'console.log("\ud83d\ude00","\\ud83d@\\ude00");' } + +issue_2569: { + input: { + new RegExp("[\udc42-\udcaa\udd74-\udd96\ude45-\ude4f\udea3-\udecc]"); + } + expect_exact: 'new RegExp("[\\udc42-\\udcaa\\udd74-\\udd96\\ude45-\\ude4f\\udea3-\\udecc]");' +} From c43118be4f8938a3c1d12f836d80c334cba76656 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 11 Dec 2017 17:39:08 +0800 Subject: [PATCH 04/42] remove unused code (#2579) fixes #2577 --- lib/utils.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 76306919..102c4789 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -43,10 +43,6 @@ "use strict"; -function slice(a, start) { - return Array.prototype.slice.call(a, start || 0); -}; - function characters(str) { return str.split(""); }; @@ -214,18 +210,6 @@ function mergeSort(array, cmp) { return _ms(array); }; -function set_difference(a, b) { - return a.filter(function(el){ - return b.indexOf(el) < 0; - }); -}; - -function set_intersection(a, b) { - return a.filter(function(el){ - return b.indexOf(el) >= 0; - }); -}; - // this function is taken from Acorn [1], written by Marijn Haverbeke // [1] https://github.com/marijnh/acorn function makePredicate(words) { From f2ad54267945ed96f4e84ade21af262c6ffd1d23 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 11 Dec 2017 18:11:09 +0800 Subject: [PATCH 05/42] fix `collapse_vars` on `switch` (#2578) --- lib/compress.js | 10 +++--- test/compress/collapse_vars.js | 58 +++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 434becb6..ce61ce9f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -873,14 +873,12 @@ merge(Compressor.prototype, { 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 (branch instanceof AST_Case) { branch.expression = branch.expression.transform(scanner); + if (side_effects || !replace_all) break; + } } - for (i = 0; !abort && i < len; i++) { - var branch = node.body[i]; - for (var j = 0, len2 = branch.body.length; j < len2; j++) - branch.body[j] = branch.body[j].transform(scanner); - } + abort = true; return node; } // Skip nodes before `candidate` as quickly as possible diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index ab86c6b4..2136b2f0 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1749,7 +1749,7 @@ for_init: { } } -switch_case: { +switch_case_1: { options = { collapse_vars: true, unused: true, @@ -1777,6 +1777,62 @@ switch_case: { } } +switch_case_2: { + options = { + collapse_vars: true, + } + input: { + var a = 1, b = 2; + switch (b++) { + case b: + var c = a; + var a; + break; + } + console.log(a); + } + expect: { + var a = 1, b = 2; + switch (b++) { + case b: + var c = a; + var a; + break; + } + console.log(a); + } + expect_stdout: "1" +} + +switch_case_3: { + options = { + collapse_vars: true, + } + input: { + var a = 1, b = 2; + switch (a) { + case a: + var b; + break; + case b: + break; + } + console.log(b); + } + expect: { + var a = 1, b = 2; + switch (a) { + case a: + var b; + break; + case b: + break; + } + console.log(b); + } + expect_stdout: "2" +} + issue_27: { options = { collapse_vars: true, From ebfd5c5c7480f35af986949f692b01ff7526b97d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 12 Dec 2017 03:30:25 +0800 Subject: [PATCH 06/42] fix `AST_VarDef.may_throw()` (#2580) --- lib/compress.js | 1 + test/compress/collapse_vars.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index ce61ce9f..14e83617 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2399,6 +2399,7 @@ merge(Compressor.prototype, { return this.expression.may_throw(compressor); }); def(AST_VarDef, function(compressor){ + if (!this.value) return false; return this.value.may_throw(compressor); }); })(function(node, func){ diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 2136b2f0..330667dd 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -3785,3 +3785,23 @@ issue_2571_2: { } expect_stdout: "undefined" } + +may_throw: { + options = { + collapse_vars: true, + } + input: { + function f() { + var a_2 = function() { + var a; + }(); + } + } + expect: { + function f() { + var a_2 = function() { + var a; + }(); + } + } +} From ddf96cfda2a3e27b977e0ed8cca3896073513186 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 12 Dec 2017 05:02:01 +0800 Subject: [PATCH 07/42] avoid `Function.prototype` pollution by `test/sandbox.js` (#2581) --- test/sandbox.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/sandbox.js b/test/sandbox.js index fe2e588e..2ce9f6e1 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -37,6 +37,7 @@ var FUNC_TOSTRING = [ ' return "[Function: " + i + "]";', " }", "}();", + 'Object.defineProperty(Function.prototype, "valueOf", { enumerable: false });', ]).join("\n"); exports.run_code = function(code) { var stdout = ""; From e008dc1bde1455eaafe04061412b0b9524212d99 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 13 Dec 2017 01:27:26 +0800 Subject: [PATCH 08/42] minor clean-up for IIFE (#2582) - faster exact type match - aggressively convert to `!` --- lib/compress.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 14e83617..00d03fd4 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -808,10 +808,8 @@ merge(Compressor.prototype, { }; function is_iife_call(node) { - if (node instanceof AST_Call && !(node instanceof AST_New)) { - return node.expression instanceof AST_Function || is_iife_call(node.expression); - } - return false; + if (node.TYPE != "Call") return false; + return node.expression instanceof AST_Function || is_iife_call(node.expression); } function is_undeclared_ref(node) { @@ -3099,14 +3097,9 @@ merge(Compressor.prototype, { } if (this.operator == "typeof" && this.expression instanceof AST_SymbolRef) return null; var expression = this.expression.drop_side_effect_free(compressor, first_in_statement); - if (first_in_statement - && this instanceof AST_UnaryPrefix - && is_iife_call(expression)) { - if (expression === this.expression && this.operator.length === 1) return this; - return make_node(AST_UnaryPrefix, this, { - operator: this.operator.length === 1 ? this.operator : "!", - expression: expression - }); + if (first_in_statement && expression && is_iife_call(expression)) { + if (expression === this.expression && this.operator == "!") return this; + return expression.negate(compressor, first_in_statement); } return expression; }); From 04cc395c353ac622d8e737c9099387d69b851d64 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 13 Dec 2017 04:52:54 +0800 Subject: [PATCH 09/42] improve `collapse_vars` on side-effect-free replacements (#2583) --- lib/compress.js | 5 ++-- test/compress/collapse_vars.js | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 00d03fd4..6aee9497 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -893,10 +893,11 @@ 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_SymbolRef && !node.is_declared(compressor) || node instanceof AST_Try || node instanceof AST_With - || parent instanceof AST_For && node !== parent.init) { + || parent instanceof AST_For && node !== parent.init + || (side_effects || !replace_all) + && (node instanceof AST_SymbolRef && !node.is_declared(compressor))) { abort = true; return node; } diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 330667dd..58405801 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -3805,3 +3805,51 @@ may_throw: { } } } + +side_effect_free_replacement: { + options = { + collapse_vars: true, + inline: true, + side_effects: true, + unused: true, + } + input: { + var b; + (function(a) { + x(a); + })(b); + } + expect: { + var b; + x(b); + } +} + +recursive_function_replacement: { + rename = true + options = { + collapse_vars: true, + inline: true, + passes: 2, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + mangle = {} + input: { + function f(a) { + return x(g(a)); + } + function g(a) { + return y(f(a)); + } + console.log(f(c)); + } + expect: { + function f(n) { + return x(y(f(n))); + } + console.log(f(c)); + } +} From 7f418978c9d39bd0827108176d817259a6e60f5c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 13 Dec 2017 18:20:53 +0800 Subject: [PATCH 10/42] recover lost opportunities from #2574 (#2584) --- lib/compress.js | 23 ++++++++++++++++- test/compress/collapse_vars.js | 46 +++++++++++++++++++++++++++------- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 6aee9497..004858ac 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -863,6 +863,7 @@ merge(Compressor.prototype, { if (scope.uses_eval || scope.uses_with) return statements; var args; var candidates = []; + var in_try = compressor.self() instanceof AST_Try; var stat_index = statements.length; var scanner = new TreeTransformer(function(node, descend) { if (abort) return node; @@ -956,7 +957,8 @@ merge(Compressor.prototype, { || side_effects && !references_in_scope(node.definition())) || (sym = lhs_or_def(node)) && (sym instanceof AST_PropAccess || sym.name in lvalues) - || may_throw && node.has_side_effects(compressor) + || 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 @@ -1192,6 +1194,25 @@ merge(Compressor.prototype, { return ref.scope === scope; }); } + + 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_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_SymbolRef) return node.definition().scope !== scope; + } + return node.has_side_effects(compressor); + } } function eliminate_spurious_blocks(statements) { diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 58405801..9dd69019 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -69,11 +69,10 @@ collapse_vars_side_effects_1: { log(x, s.charAt(i++), y, 7); } function f4() { - var log = console.log.bind(console), - i = 10, + var i = 10, x = i += 2, y = i += 3; - log(x, i += 4, y, i); + console.log.bind(console)(x, i += 4, y, i); } f1(), f2(), f3(), f4(); } @@ -676,8 +675,8 @@ collapse_vars_lvalues: { function f4(x) { var a = (x -= 3); return x + a; } function f5(x) { var w = e1(), v = e2(), c = v = --x; return (w = x) - c; } function f6(x) { var w = e1(), v = e2(); return (v = --x) - (w = x); } - function f7(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } - function f8(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } + function f7(x) { var w = e1(); return (w = x) - (e2() - x); } + function f8(x) { var w = e1(); return (w = x) - (e2() - x); } function f9(x) { var w = e1(); return e2() - x - (w = x); } } } @@ -2251,7 +2250,7 @@ issue_315: { expect: { console.log(function() { var w, _i, _len, _ref, _results; - for (_ref = "test".trim().split(" "), _results = [], _i = 0, _len = _ref.length; _i < _len ; _i++) + for (_results = [], _i = 0, _len = (_ref = "test".trim().split(" ")).length; _i < _len ; _i++) w = _ref[_i], _results.push(w.toLowerCase()); return _results; }()); @@ -3164,8 +3163,8 @@ issue_2437: { return Object.defineProperty(XMLHttpRequest.prototype, "onreadystatechange", xhrDesc || {}), result; } - var req = new XMLHttpRequest(), detectFunc = function() {}; - req.onreadystatechange = detectFunc; + var req, detectFunc = function() {}; + (req = new XMLHttpRequest()).onreadystatechange = detectFunc; result = req[SYMBOL_FAKE_ONREADYSTATECHANGE_1] === detectFunc; req.onreadystatechange = null; }(); @@ -3786,7 +3785,7 @@ issue_2571_2: { expect_stdout: "undefined" } -may_throw: { +may_throw_1: { options = { collapse_vars: true, } @@ -3806,6 +3805,35 @@ may_throw: { } } +may_throw_2: { + options = { + collapse_vars: true, + unused: true, + } + input: { + function f(b) { + try { + var a = x(); + ++b; + return b(a); + } catch(e) {} + console.log(b); + } + f(0); + } + expect: { + function f(b) { + try { + var a = x(); + return (++b)(a); + } catch(e) {} + console.log(b); + } + f(0); + } + expect_stdout: "0" +} + side_effect_free_replacement: { options = { collapse_vars: true, From ef618332ea92db57e59f90f166035a0e7cf8a509 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 14 Dec 2017 01:20:36 +0800 Subject: [PATCH 11/42] fold `cascade` functionality into `collapse_vars` (#2586) --- README.md | 3 - lib/compress.js | 109 +++++++------------ test/compress/asm.js | 1 - test/compress/collapse_vars.js | 169 +++++++++++++++++++++++------- test/compress/conditionals.js | 4 +- test/compress/drop-unused.js | 10 +- test/compress/functions.js | 6 +- test/compress/issue-1034.js | 12 +-- test/compress/issue-1105.js | 2 - test/compress/issue-1261.js | 78 +++++++------- test/compress/issue-1275.js | 1 - test/compress/issue-1447.js | 1 - test/compress/issue-1639.js | 6 +- test/compress/issue-1656.js | 1 - test/compress/issue-281.js | 4 +- test/compress/issue-368.js | 4 +- test/compress/issue-892.js | 1 - test/compress/issue-976.js | 2 +- test/compress/issue-979.js | 4 +- test/compress/pure_getters.js | 12 +-- test/compress/reduce_vars.js | 2 +- test/compress/return_undefined.js | 1 - test/compress/sequences.js | 43 +++++--- 23 files changed, 269 insertions(+), 207 deletions(-) diff --git a/README.md b/README.md index 8a34f6e4..ef496178 100644 --- a/README.md +++ b/README.md @@ -601,9 +601,6 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `booleans` (default: `true`) -- various optimizations for boolean context, for example `!!a ? b : c → a ? b : c` -- `cascade` (default: `true`) -- small optimization for sequences, transform - `x, x` into `x` and `x = something(), x` into `x = something()` - - `collapse_vars` (default: `true`) -- Collapse single-use non-constant variables, side effects permitting. diff --git a/lib/compress.js b/lib/compress.js index 004858ac..6e984ffa 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -49,7 +49,6 @@ function Compressor(options, false_by_default) { TreeTransformer.call(this, this.before, this.after); this.options = defaults(options, { booleans : !false_by_default, - cascade : !false_by_default, collapse_vars : !false_by_default, comparisons : !false_by_default, conditionals : !false_by_default, @@ -884,6 +883,8 @@ merge(Compressor.prototype, { if (!hit) { if (node === candidate) { hit = true; + stop_after = find_stop(node, 0); + if (stop_after === node) abort = true; return node; } return; @@ -963,12 +964,13 @@ merge(Compressor.prototype, { && (parent instanceof AST_Binary && lazy_op(parent.operator) || parent instanceof AST_Conditional || parent instanceof AST_If)) { - if (!(node instanceof AST_Scope)) descend(node, scanner); - abort = true; - return node; + stop_after = node; + if (node instanceof AST_Scope) abort = true; } // Skip (non-executed) functions if (node instanceof AST_Scope) return node; + }, function(node) { + if (!abort && stop_after === node) abort = true; }); var multi_replacer = new TreeTransformer(function(node) { if (abort) return node; @@ -1003,6 +1005,7 @@ merge(Compressor.prototype, { while (candidates.length > 0) { var candidate = candidates.pop(); var value_def = null; + var stop_after = 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 @@ -1094,19 +1097,43 @@ merge(Compressor.prototype, { if (expr instanceof AST_Assign && !expr.left.has_side_effects(compressor) || expr instanceof AST_Unary && (expr.operator == "++" || expr.operator == "--")) { candidates.push(expr); - } else if (expr instanceof AST_Sequence) { - expr.expressions.forEach(extract_candidates); + } else if (expr instanceof AST_Case) { + extract_candidates(expr.expression); + } else if (expr instanceof AST_Conditional) { + extract_candidates(expr.condition); + extract_candidates(expr.consequent); + extract_candidates(expr.alternative); } else if (expr instanceof AST_Definitions) { expr.definitions.forEach(function(var_def) { if (var_def.value) candidates.push(var_def); }); + } else if (expr instanceof AST_Exit) { + if (expr.value) extract_candidates(expr.value); + } else if (expr instanceof AST_For) { + if (expr.init) extract_candidates(expr.init); + } else if (expr instanceof AST_If) { + extract_candidates(expr.condition); + } else if (expr instanceof AST_Sequence) { + expr.expressions.forEach(extract_candidates); } else if (expr instanceof AST_SimpleStatement) { extract_candidates(expr.body); - } else if (expr instanceof AST_For && expr.init) { - extract_candidates(expr.init); + } else if (expr instanceof AST_Switch) { + extract_candidates(expr.expression); + expr.body.forEach(extract_candidates); } } + function find_stop(node, level) { + var parent = scanner.parent(level); + if (parent instanceof AST_Case) return node; + if (parent instanceof AST_Conditional) return node; + if (parent instanceof AST_Exit) return node; + if (parent instanceof AST_If) return node; + if (parent instanceof AST_Sequence) return find_stop(parent, level + 1); + if (parent instanceof AST_Switch) return node; + return null; + } + function mangleable_var(var_def) { var value = var_def.value; if (!(value instanceof AST_SymbolRef)) return; @@ -3898,7 +3925,6 @@ merge(Compressor.prototype, { filter_for_side_effects(); var end = expressions.length - 1; trim_right_for_undefined(); - if (end > 0 && compressor.option("cascade")) trim_left_for_assignment(); if (end == 0) { self = maintain_this_binding(compressor.parent(), compressor.self(), expressions[0]); if (!(self instanceof AST_Sequence)) self = self.optimize(compressor); @@ -3929,71 +3955,6 @@ merge(Compressor.prototype, { expressions.length = end + 1; } } - - function trim_left_for_assignment() { - for (var i = 0, j = 1; j <= end; j++) { - var left = expressions[i]; - var cdr = expressions[j]; - if (left instanceof AST_Assign - && !left.left.has_side_effects(compressor)) { - left = left.left; - } else if (left instanceof AST_Unary - && (left.operator == "++" || left.operator == "--")) { - left = left.expression; - } else left = null; - if (!left || is_lhs_read_only(left) || left.has_side_effects(compressor)) { - expressions[++i] = cdr; - continue; - } - var parent = null, field; - expressions[j] = cdr = cdr.clone(); - while (true) { - if (cdr.equivalent_to(left)) { - var car = expressions[i]; - if (car instanceof AST_UnaryPostfix) { - car = make_node(AST_UnaryPrefix, car, { - operator: car.operator, - expression: left - }); - } else { - car.write_only = false; - } - if (parent) { - parent[field] = car; - expressions[i] = expressions[j]; - } else { - expressions[i] = car; - } - break; - } - if (cdr instanceof AST_Binary && !(cdr instanceof AST_Assign)) { - if (cdr.left.is_constant()) { - if (lazy_op(cdr.operator)) { - expressions[++i] = expressions[j]; - break; - } - field = "right"; - } else { - field = "left"; - } - } else if (cdr instanceof AST_Call - && !(left instanceof AST_PropAccess && cdr.expression.equivalent_to(left)) - || cdr instanceof AST_PropAccess - || cdr instanceof AST_Unary && !unary_side_effects(cdr.operator)) { - field = "expression"; - } else if (cdr instanceof AST_Conditional) { - field = "condition"; - } else { - expressions[++i] = expressions[j]; - break; - } - parent = cdr; - cdr = cdr[field] = cdr[field].clone(); - } - } - end = i; - expressions.length = end + 1; - } }); AST_Unary.DEFMETHOD("lift_sequences", function(compressor){ diff --git a/test/compress/asm.js b/test/compress/asm.js index 527e6b43..a9047c5d 100644 --- a/test/compress/asm.js +++ b/test/compress/asm.js @@ -16,7 +16,6 @@ asm_mixed: { hoist_vars : true, if_return : true, join_vars : true, - cascade : true, side_effects : true, negate_iife : true }; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 9dd69019..1323116a 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -2,7 +2,7 @@ collapse_vars_side_effects_1: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -83,7 +83,7 @@ collapse_vars_side_effects_2: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function fn(x) { return console.log(x), x; } @@ -151,7 +151,7 @@ collapse_vars_issue_721: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true, passes:2 } input: { @@ -218,7 +218,7 @@ collapse_vars_properties: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -246,7 +246,7 @@ collapse_vars_if: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -297,7 +297,7 @@ collapse_vars_while: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:false, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -346,7 +346,7 @@ collapse_vars_do_while: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:false, loops:false, unused:"keep_assign", - hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, cascade:true, + hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { @@ -422,7 +422,7 @@ collapse_vars_do_while_drop_assign: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:false, loops:false, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f1(y) { @@ -497,7 +497,7 @@ collapse_vars_seq: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { var f1 = function(x, y) { @@ -505,20 +505,23 @@ collapse_vars_seq: { a = z, b = 7; return a + b; }; + console.log(f1(1, 2)); } expect: { var f1 = function(x, y) { - var a, b, r = x + y; - return a = r * r - r, b = 7, a + b + var r = x + y; + return r * r - r + 7; }; + console.log(f1(1, 2)); } + expect_stdout: "13" } collapse_vars_throw: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { var f1 = function(x, y) { @@ -526,20 +529,31 @@ collapse_vars_throw: { a = z, b = 7; throw a + b; }; + try { + f1(1, 2); + } catch (e) { + console.log(e); + } } expect: { var f1 = function(x, y) { - var a, b, r = x + y; - throw a = r * r - r, b = 7, a + b + var r = x + y; + throw r * r - r + 7; }; + try { + f1(1, 2); + } catch (e) { + console.log(e); + } } + expect_stdout: "13" } collapse_vars_switch: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f1() { @@ -579,7 +593,7 @@ collapse_vars_assignment: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function log(x) { return console.log(x), x; } @@ -652,7 +666,7 @@ collapse_vars_lvalues: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:"keep_assign", - hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, cascade:true, + hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { @@ -685,7 +699,7 @@ collapse_vars_lvalues_drop_assign: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, passes:3 + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, passes:3 } input: { function f0(x) { var i = ++x; return x += i; } @@ -717,7 +731,7 @@ collapse_vars_misc1: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -765,7 +779,7 @@ collapse_vars_self_reference: { collapse_vars:true, unused:false, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { // avoid bug in self-referential declaration. @@ -795,7 +809,7 @@ collapse_vars_repeated: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -838,7 +852,7 @@ collapse_vars_closures: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -866,7 +880,7 @@ collapse_vars_unary: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f0(o, p) { @@ -929,7 +943,7 @@ collapse_vars_try: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -985,7 +999,7 @@ collapse_vars_array: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f1(x, y) { @@ -1019,7 +1033,7 @@ collapse_vars_object: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f0(x, y) { @@ -1087,7 +1101,7 @@ collapse_vars_eval_and_with: { options = { collapse_vars:true, sequences:false, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { // Don't attempt to collapse vars in presence of eval() or with statement. @@ -1127,7 +1141,7 @@ collapse_vars_constants: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true } input: { @@ -1165,7 +1179,7 @@ collapse_vars_arguments: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + keep_fargs:true, if_return:true, join_vars:true, side_effects:true, toplevel:true, reduce_funcs: true, reduce_vars:true } input: { @@ -1188,7 +1202,7 @@ collapse_vars_short_circuit: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f0(x) { var a = foo(), b = bar(); return b || x; } @@ -1241,7 +1255,6 @@ collapse_vars_short_circuited_conditions: { keep_fargs: true, if_return: false, join_vars: true, - cascade: true, side_effects: true, } input: { @@ -1279,7 +1292,6 @@ collapse_vars_short_circuited_conditions: { collapse_vars_regexp: { options = { booleans: true, - cascade: true, collapse_vars: true, comparisons: true, conditionals: true, @@ -1443,7 +1455,6 @@ issue_1605_2: { issue_1631_1: { options = { - cascade: true, collapse_vars: true, hoist_funs: true, join_vars: true, @@ -1479,7 +1490,6 @@ issue_1631_1: { issue_1631_2: { options = { - cascade: true, collapse_vars: true, hoist_funs: true, join_vars: true, @@ -1515,7 +1525,6 @@ issue_1631_2: { issue_1631_3: { options = { - cascade: true, collapse_vars: true, hoist_funs: true, join_vars: true, @@ -1690,7 +1699,7 @@ var_defs: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { var f1 = function(x, y) { @@ -3881,3 +3890,91 @@ recursive_function_replacement: { console.log(f(c)); } } + +cascade_conditional: { + options = { + collapse_vars: true, + } + input: { + function f(a, b) { + (a = x(), a) ? a++ : (b = y(a), b(a)); + } + } + expect: { + function f(a, b) { + (a = x()) ? a++ : (b = y(a))(a); + } + } +} + +cascade_if_1: { + options = { + collapse_vars: true, + } + input: { + var a; + if (a = x(), a) + if (a == y()) z(); + } + expect: { + var a; + if (a = x()) + if (a == y()) z(); + } +} + +cascade_if_2: { + options = { + collapse_vars: true, + } + input: { + function f(a, b) { + if (a(), b = x()) return b; + } + } + expect: { + function f(a, b) { + if (a(), b = x()) return b; + } + } +} + +cascade_return: { + options = { + collapse_vars: true, + } + input: { + function f(a) { + return a = x(); + return a; + } + } + expect: { + function f(a) { + return a = x(); + return a; + } + } +} + +cascade_switch: { + options = { + collapse_vars: true, + } + input: { + function f(a, b) { + switch(a = x(), a) { + case a = x(), b(a): + break; + } + } + } + expect: { + function f(a, b) { + switch(a = x()) { + case b(a = x()): + break; + } + } + } +} diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 143ece4a..4d61d39f 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -649,7 +649,7 @@ ternary_boolean_consequent: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f1() { return a == b ? true : x; } @@ -677,7 +677,7 @@ ternary_boolean_alternative: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f1() { return a == b ? x : true; } diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index f9ef7877..275e0f76 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -685,7 +685,7 @@ drop_value: { issue_1539: { options = { - cascade: true, + collapse_vars: true, sequences: true, side_effects: true, unused: true, @@ -732,7 +732,7 @@ vardef_value: { assign_binding: { options = { - cascade: true, + collapse_vars: true, side_effects: true, unused: true, } @@ -1273,7 +1273,7 @@ issue_2226_1: { issue_2226_2: { options = { - cascade: true, + collapse_vars: true, sequences: true, side_effects: true, unused: true, @@ -1286,8 +1286,8 @@ issue_2226_2: { } expect: { console.log(function(a, b) { - return a += b; - }(1, 2)); + return a += 2; + }(1)); } expect_stdout: "3" } diff --git a/test/compress/functions.js b/test/compress/functions.js index 15727fc2..7e87692d 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -21,7 +21,7 @@ iifes_returning_constants_keep_fargs_true: { join_vars : true, reduce_funcs : true, reduce_vars : true, - cascade : true, + collapse_vars : true, inline : true, } input: { @@ -58,7 +58,7 @@ iifes_returning_constants_keep_fargs_false: { join_vars : true, reduce_funcs : true, reduce_vars : true, - cascade : true, + collapse_vars : true, inline : true, } input: { @@ -423,9 +423,9 @@ inner_ref: { issue_2107: { options = { - cascade: true, collapse_vars: true, inline: true, + passes: 3, sequences: true, side_effects: true, unused: true, diff --git a/test/compress/issue-1034.js b/test/compress/issue-1034.js index f312408c..860a597f 100644 --- a/test/compress/issue-1034.js +++ b/test/compress/issue-1034.js @@ -2,7 +2,7 @@ non_hoisted_function_after_return: { options = { hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, - if_return: true, join_vars: true, cascade: true, side_effects: true + if_return: true, join_vars: true, side_effects: true } input: { function foo(x) { @@ -38,7 +38,7 @@ non_hoisted_function_after_return_2a: { options = { hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, - if_return: true, join_vars: true, cascade: true, side_effects: true, + if_return: true, join_vars: true, side_effects: true, collapse_vars: false, passes: 2, warnings: "verbose" } input: { @@ -85,7 +85,7 @@ non_hoisted_function_after_return_2b: { options = { hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, - if_return: true, join_vars: true, cascade: true, side_effects: true, + if_return: true, join_vars: true, side_effects: true, collapse_vars: false } input: { @@ -123,7 +123,7 @@ non_hoisted_function_after_return_strict: { options = { hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, - if_return: true, join_vars: true, cascade: true, side_effects: true + if_return: true, join_vars: true, side_effects: true } input: { "use strict"; @@ -164,7 +164,7 @@ non_hoisted_function_after_return_2a_strict: { options = { hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, - if_return: true, join_vars: true, cascade: true, side_effects: true, + if_return: true, join_vars: true, side_effects: true, collapse_vars: false, passes: 2, warnings: "verbose" } input: { @@ -216,7 +216,7 @@ non_hoisted_function_after_return_2b_strict: { options = { hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, - if_return: true, join_vars: true, cascade: true, side_effects: true, + if_return: true, join_vars: true, side_effects: true, collapse_vars: false } input: { diff --git a/test/compress/issue-1105.js b/test/compress/issue-1105.js index ea957930..151ca810 100644 --- a/test/compress/issue-1105.js +++ b/test/compress/issue-1105.js @@ -190,7 +190,6 @@ assorted_Infinity_NaN_undefined_in_with_scope: { keep_fargs: true, if_return: true, join_vars: true, - cascade: true, side_effects: true, sequences: false, keep_infinity: false, @@ -253,7 +252,6 @@ assorted_Infinity_NaN_undefined_in_with_scope_keep_infinity: { keep_fargs: true, if_return: true, join_vars: true, - cascade: true, side_effects: true, sequences: false, keep_infinity: true, diff --git a/test/compress/issue-1261.js b/test/compress/issue-1261.js index 994a00b3..9f4f466f 100644 --- a/test/compress/issue-1261.js +++ b/test/compress/issue-1261.js @@ -8,7 +8,6 @@ pure_function_calls: { unused : true, if_return : true, join_vars : true, - cascade : true, negate_iife : true, } input: { @@ -49,13 +48,13 @@ pure_function_calls: { a.b(), f.g(); } expect_warnings: [ - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:17,8]", - "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:17,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:30,37]", - "WARN: Dropping unused variable iife2 [test/compress/issue-1261.js:30,16]", - "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:28,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:38,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:39,31]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:16,8]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:16,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:29,37]", + "WARN: Dropping unused variable iife2 [test/compress/issue-1261.js:29,16]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:27,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:37,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:38,31]", ] } @@ -69,7 +68,6 @@ pure_function_calls_toplevel: { unused : true, if_return : true, join_vars : true, - cascade : true, negate_iife : true, toplevel : true, } @@ -112,17 +110,17 @@ pure_function_calls_toplevel: { a.b(), f.g(); } expect_warnings: [ - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:79,8]", - "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:79,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:92,37]", - "WARN: Dropping unused variable iife2 [test/compress/issue-1261.js:92,16]", - "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:90,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:107,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:108,31]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:84,33]", - "WARN: Dropping unused variable iife1 [test/compress/issue-1261.js:84,12]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:100,45]", - "WARN: Dropping unused variable MyClass [test/compress/issue-1261.js:100,12]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:77,8]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:77,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:90,37]", + "WARN: Dropping unused variable iife2 [test/compress/issue-1261.js:90,16]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:88,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:105,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:106,31]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:82,33]", + "WARN: Dropping unused variable iife1 [test/compress/issue-1261.js:82,12]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:98,45]", + "WARN: Dropping unused variable MyClass [test/compress/issue-1261.js:98,12]", ] } @@ -157,29 +155,29 @@ should_warn: { baz(); } expect_warnings: [ - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:137,61]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:137,23]", - "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:137,23]", - "WARN: Boolean || always true [test/compress/issue-1261.js:138,23]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:135,61]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:135,23]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:135,23]", + "WARN: Boolean || always true [test/compress/issue-1261.js:136,23]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:136,23]", + "WARN: Condition always true [test/compress/issue-1261.js:136,23]", + "WARN: Condition left of || always true [test/compress/issue-1261.js:137,8]", + "WARN: Condition always true [test/compress/issue-1261.js:137,8]", + "WARN: Boolean && always false [test/compress/issue-1261.js:138,23]", "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:138,23]", - "WARN: Condition always true [test/compress/issue-1261.js:138,23]", - "WARN: Condition left of || always true [test/compress/issue-1261.js:139,8]", - "WARN: Condition always true [test/compress/issue-1261.js:139,8]", - "WARN: Boolean && always false [test/compress/issue-1261.js:140,23]", + "WARN: Condition always false [test/compress/issue-1261.js:138,23]", + "WARN: Condition left of && always false [test/compress/issue-1261.js:139,8]", + "WARN: Condition always false [test/compress/issue-1261.js:139,8]", + "WARN: + in boolean context always true [test/compress/issue-1261.js:140,23]", "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:140,23]", - "WARN: Condition always false [test/compress/issue-1261.js:140,23]", - "WARN: Condition left of && always false [test/compress/issue-1261.js:141,8]", - "WARN: Condition always false [test/compress/issue-1261.js:141,8]", - "WARN: + in boolean context always true [test/compress/issue-1261.js:142,23]", + "WARN: Condition always true [test/compress/issue-1261.js:140,23]", + "WARN: + in boolean context always true [test/compress/issue-1261.js:141,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:141,31]", + "WARN: Condition always true [test/compress/issue-1261.js:141,8]", "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:142,23]", - "WARN: Condition always true [test/compress/issue-1261.js:142,23]", - "WARN: + in boolean context always true [test/compress/issue-1261.js:143,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:143,31]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:143,24]", "WARN: Condition always true [test/compress/issue-1261.js:143,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:144,23]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:145,24]", - "WARN: Condition always true [test/compress/issue-1261.js:145,8]", - "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:146,31]", - "WARN: Condition always false [test/compress/issue-1261.js:146,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:144,31]", + "WARN: Condition always false [test/compress/issue-1261.js:144,8]", ] } diff --git a/test/compress/issue-1275.js b/test/compress/issue-1275.js index 5d4f5b70..2553c74f 100644 --- a/test/compress/issue-1275.js +++ b/test/compress/issue-1275.js @@ -9,7 +9,6 @@ string_plus_optimization: { unused : true, if_return : true, join_vars : true, - cascade : true, hoist_funs : true, }; input: { diff --git a/test/compress/issue-1447.js b/test/compress/issue-1447.js index a7f35e5a..0a765685 100644 --- a/test/compress/issue-1447.js +++ b/test/compress/issue-1447.js @@ -32,7 +32,6 @@ conditional_false_stray_else_in_loop: { hoist_vars : true, join_vars : true, if_return : true, - cascade : true, conditionals : false, } input: { diff --git a/test/compress/issue-1639.js b/test/compress/issue-1639.js index fc3db983..80d45a76 100644 --- a/test/compress/issue-1639.js +++ b/test/compress/issue-1639.js @@ -2,7 +2,7 @@ issue_1639_1: { options = { booleans: true, - cascade: true, + collapse_vars: true, conditionals: true, evaluate: true, join_vars: true, @@ -35,7 +35,7 @@ issue_1639_1: { issue_1639_2: { options = { booleans: true, - cascade: true, + collapse_vars: true, conditionals: true, evaluate: true, join_vars: true, @@ -68,7 +68,7 @@ issue_1639_2: { issue_1639_3: { options = { booleans: true, - cascade: true, + collapse_vars: true, conditionals: true, evaluate: true, sequences: true, diff --git a/test/compress/issue-1656.js b/test/compress/issue-1656.js index 27d87652..e44e2094 100644 --- a/test/compress/issue-1656.js +++ b/test/compress/issue-1656.js @@ -1,7 +1,6 @@ f7: { options = { booleans: true, - cascade: true, collapse_vars: true, comparisons: true, conditionals: true, diff --git a/test/compress/issue-281.js b/test/compress/issue-281.js index 6a93136f..1e532dfb 100644 --- a/test/compress/issue-281.js +++ b/test/compress/issue-281.js @@ -453,7 +453,7 @@ pure_annotation_2: { drop_fargs: { options = { - cascade: true, + collapse_vars: true, inline: true, keep_fargs: false, side_effects: true, @@ -476,7 +476,7 @@ drop_fargs: { keep_fargs: { options = { - cascade: true, + collapse_vars: true, inline: true, keep_fargs: true, side_effects: true, diff --git a/test/compress/issue-368.js b/test/compress/issue-368.js index 5960aa64..b0491c29 100644 --- a/test/compress/issue-368.js +++ b/test/compress/issue-368.js @@ -1,6 +1,6 @@ collapse: { options = { - cascade: true, + collapse_vars: true, sequences: true, side_effects: true, unused: true, @@ -41,7 +41,7 @@ collapse: { return void 0 !== ('function' === typeof b ? b() : b) && c(); } function f2(b) { - return b = c(), 'stirng' == typeof ('function' === typeof b ? b() : b) && d(); + return 'stirng' == typeof ('function' === typeof (b = c()) ? b() : b) && d(); } function f3(c) { var a; diff --git a/test/compress/issue-892.js b/test/compress/issue-892.js index b6938c42..81df1cab 100644 --- a/test/compress/issue-892.js +++ b/test/compress/issue-892.js @@ -18,7 +18,6 @@ dont_mangle_arguments: { hoist_vars : true, if_return : true, join_vars : true, - cascade : true, side_effects : true, negate_iife : false }; diff --git a/test/compress/issue-976.js b/test/compress/issue-976.js index b711051b..54d7dad5 100644 --- a/test/compress/issue-976.js +++ b/test/compress/issue-976.js @@ -2,7 +2,7 @@ eval_collapse_vars: { options = { collapse_vars:true, sequences:false, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true }; input: { function f1() { diff --git a/test/compress/issue-979.js b/test/compress/issue-979.js index 7ed5801d..b2500126 100644 --- a/test/compress/issue-979.js +++ b/test/compress/issue-979.js @@ -2,7 +2,7 @@ issue979_reported: { options = { sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f1() { @@ -32,7 +32,7 @@ issue979_test_negated_is_best: { options = { sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, side_effects:true } input: { function f3() { diff --git a/test/compress/pure_getters.js b/test/compress/pure_getters.js index 5c16b2cd..4174bc1b 100644 --- a/test/compress/pure_getters.js +++ b/test/compress/pure_getters.js @@ -185,7 +185,7 @@ impure_getter_2: { issue_2110_1: { options = { - cascade: true, + collapse_vars: true, pure_getters: "strict", sequences: true, side_effects: true, @@ -274,7 +274,7 @@ set_immutable_1: { set_immutable_2: { options = { - cascade: true, + collapse_vars: true, conditionals: true, pure_getters: "strict", reduce_funcs: true, @@ -324,7 +324,7 @@ set_immutable_3: { set_immutable_4: { options = { - cascade: true, + collapse_vars: true, conditionals: true, pure_getters: "strict", reduce_funcs: true, @@ -375,7 +375,7 @@ set_mutable_1: { set_mutable_2: { options = { - cascade: true, + collapse_vars: true, conditionals: true, pure_getters: "strict", reduce_funcs: true, @@ -400,7 +400,7 @@ set_mutable_2: { issue_2313_1: { options = { - cascade: true, + collapse_vars: true, conditionals: true, pure_getters: "strict", sequences: true, @@ -446,7 +446,7 @@ issue_2313_1: { issue_2313_2: { options = { - cascade: true, + collapse_vars: true, conditionals: true, pure_getters: true, sequences: true, diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 6c8bd639..504ce6f0 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2262,7 +2262,7 @@ redefine_farg_2: { redefine_farg_3: { options = { - cascade: true, + collapse_vars: true, evaluate: true, inline: true, keep_fargs: false, diff --git a/test/compress/return_undefined.js b/test/compress/return_undefined.js index 4d2b4257..c7e09067 100644 --- a/test/compress/return_undefined.js +++ b/test/compress/return_undefined.js @@ -17,7 +17,6 @@ return_undefined: { keep_fnames : false, hoist_vars : true, join_vars : true, - cascade : true, negate_iife : true }; input: { diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 26f38c25..81b06881 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -317,7 +317,7 @@ unsafe_undefined: { issue_1685: { options = { - cascade: true, + collapse_vars: true, side_effects: true, } input: { @@ -341,7 +341,7 @@ issue_1685: { func_def_1: { options = { - cascade: true, + collapse_vars: true, side_effects: true, } input: { @@ -361,7 +361,7 @@ func_def_1: { func_def_2: { options = { - cascade: true, + collapse_vars: true, side_effects: true, } input: { @@ -379,7 +379,7 @@ func_def_2: { func_def_3: { options = { - cascade: true, + collapse_vars: true, side_effects: true, } input: { @@ -401,7 +401,7 @@ func_def_3: { func_def_4: { options = { - cascade: true, + collapse_vars: true, side_effects: true, } input: { @@ -427,7 +427,7 @@ func_def_4: { func_def_5: { options = { - cascade: true, + collapse_vars: true, side_effects: true, } input: { @@ -634,7 +634,7 @@ side_effects: { side_effects_cascade_1: { options = { - cascade: true, + collapse_vars: true, conditionals: true, sequences: true, side_effects: true, @@ -655,7 +655,7 @@ side_effects_cascade_1: { side_effects_cascade_2: { options = { - cascade: true, + collapse_vars: true, side_effects: true, } input: { @@ -677,7 +677,7 @@ side_effects_cascade_2: { side_effects_cascade_3: { options = { - cascade: true, + collapse_vars: true, conditionals: true, side_effects: true, } @@ -692,14 +692,14 @@ side_effects_cascade_3: { expect: { function f(a, b) { !(b += a) && ((b = a) || (b -= a, b ^= a)), - --a; + a--; } } } issue_27: { options = { - cascade: true, + collapse_vars: true, passes: 2, sequences: true, side_effects: true, @@ -722,7 +722,7 @@ issue_27: { issue_2062: { options = { booleans: true, - cascade: true, + collapse_vars: true, conditionals: true, side_effects: true, } @@ -741,7 +741,7 @@ issue_2062: { issue_2313: { options = { - cascade: true, + collapse_vars: true, sequences: true, side_effects: true, } @@ -779,3 +779,20 @@ issue_2313: { } expect_stdout: "2 1" } + +cascade_assignment_in_return: { + options = { + collapse_vars: true, + unused: true, + } + input: { + function f(a, b) { + return a = x(), b(a); + } + } + expect: { + function f(a, b) { + return b(x()); + } + } +} From 9a137e8613e49066a54f2a1b734a559c05338f11 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 14 Dec 2017 02:59:59 +0800 Subject: [PATCH 12/42] drop local assign-only variable in `return` (#2587) --- lib/compress.js | 18 +++++++ test/compress/collapse_vars.js | 10 ++-- test/compress/dead-code.js | 90 ++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 5 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 6e984ffa..559fb51a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4666,6 +4666,24 @@ merge(Compressor.prototype, { var ASSIGN_OPS = [ '+', '-', '/', '*', '%', '>>', '<<', '>>>', '|', '^', '&' ]; var ASSIGN_OPS_COMMUTATIVE = [ '*', '|', '^', '&' ]; OPT(AST_Assign, function(self, compressor){ + if (compressor.option("dead_code") + && self.left instanceof AST_SymbolRef + && self.left.definition().scope === compressor.find_parent(AST_Lambda)) { + var level = 0, node, parent = self; + do { + node = parent; + parent = compressor.parent(level++); + if (parent instanceof AST_Exit) { + if (self.operator == "=") return self.right; + return make_node(AST_Binary, self, { + operator: self.operator.slice(0, -1), + left: self.left, + right: self.right + }).optimize(compressor); + } + } while (parent instanceof AST_Binary && parent.right === node + || parent instanceof AST_Sequence && parent.tail_node() === node); + } self = self.lift_sequences(compressor); if (self.operator == "=" && self.left instanceof AST_SymbolRef && self.right instanceof AST_Binary) { // x = expr1 OP expr2 diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 1323116a..bd1362b1 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -591,7 +591,7 @@ collapse_vars_switch: { collapse_vars_assignment: { options = { - collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + collapse_vars:true, sequences:true, properties:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, side_effects:true } @@ -639,7 +639,7 @@ collapse_vars_assignment: { return a = a; } function f1(c) { - return 1 - 3 / c + return 1 - 3 / c; } function f2(c) { return log(c = 3 / c - 7); @@ -664,7 +664,7 @@ collapse_vars_assignment: { collapse_vars_lvalues: { options = { - collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + collapse_vars:true, sequences:true, properties:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:"keep_assign", hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, side_effects:true @@ -697,7 +697,7 @@ collapse_vars_lvalues: { collapse_vars_lvalues_drop_assign: { options = { - collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + collapse_vars:true, sequences:true, properties:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, side_effects:true, passes:3 } @@ -729,7 +729,7 @@ collapse_vars_lvalues_drop_assign: { collapse_vars_misc1: { options = { - collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + collapse_vars:true, sequences:true, properties:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, side_effects:true, reduce_funcs: true, reduce_vars:true diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 9baf9984..ca7ad5c0 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -417,3 +417,93 @@ global_fns: { "RangeError", ] } + +collapse_vars_assignment: { + options = { + collapse_vars: true, + dead_code: true, + passes: 2, + unused: true, + } + input: { + function f0(c) { + var a = 3 / c; + return a = a; + } + } + expect: { + function f0(c) { + return 3 / c; + } + } +} + +collapse_vars_lvalues_drop_assign: { + options = { + collapse_vars: true, + dead_code: true, + unused: true, + } + input: { + function f0(x) { var i = ++x; return x += i; } + function f1(x) { var a = (x -= 3); return x += a; } + function f2(x) { var z = x, a = ++z; return z += a; } + } + expect: { + function f0(x) { var i = ++x; return x + i; } + function f1(x) { var a = (x -= 3); return x + a; } + function f2(x) { var z = x, a = ++z; return z + a; } + } +} + +collapse_vars_misc1: { + options = { + collapse_vars: true, + dead_code: true, + unused: true, + } + input: { + function f10(x) { var a = 5, b = 3; return a += b; } + function f11(x) { var a = 5, b = 3; return a += --b; } + } + expect: { + function f10(x) { return 5 + 3; } + function f11(x) { var b = 3; return 5 + --b; } + } +} + +return_assignment: { + options = { + dead_code: true, + unused: true, + } + input: { + function f1(a, b, c) { + return a = x(), b = y(), b = a && (c >>= 5); + } + function f2() { + return e = x(); + } + function f3(e) { + return e = x(); + } + function f4() { + var e; + return e = x(); + } + } + expect: { + function f1(a, b, c) { + return a = x(), y(), a && (c >> 5); + } + function f2() { + return e = x(); + } + function f3(e) { + return x(); + } + function f4() { + return x(); + } + } +} From 8266993c6e0a78c4872cbcb81072094a4a7dba2d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 14 Dec 2017 04:38:21 +0800 Subject: [PATCH 13/42] fix `dead_code` on `return`/`throw` within `try` (#2588) --- lib/compress.js | 13 ++++ test/compress/dead-code.js | 146 +++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 559fb51a..e394e8b2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4674,6 +4674,11 @@ merge(Compressor.prototype, { node = parent; parent = compressor.parent(level++); if (parent instanceof AST_Exit) { + var try_node = find_try(level); + if (try_node) { + if (try_node.bfinally) break; + if (parent instanceof AST_Throw && try_node.bcatch) break; + } if (self.operator == "=") return self.right; return make_node(AST_Binary, self, { operator: self.operator.slice(0, -1), @@ -4704,6 +4709,14 @@ merge(Compressor.prototype, { } } return self; + + function find_try(level) { + var scope = self.left.definition().scope; + var parent; + while ((parent = compressor.parent(level++)) !== scope) { + if (parent instanceof AST_Try) return parent; + } + } }); OPT(AST_Conditional, function(self, compressor){ diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index ca7ad5c0..9612b92b 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -491,6 +491,20 @@ return_assignment: { var e; return e = x(); } + function f5(a) { + try { + return a = x(); + } catch (b) { + console.log(a); + } + } + function f6(a) { + try { + return a = x(); + } finally { + console.log(a); + } + } } expect: { function f1(a, b, c) { @@ -505,5 +519,137 @@ return_assignment: { function f4() { return x(); } + function f5(a) { + try { + return x(); + } catch (b) { + console.log(a); + } + } + function f6(a) { + try { + return a = x(); + } finally { + console.log(a); + } + } + } +} + +throw_assignment: { + options = { + dead_code: true, + unused: true, + } + input: { + function f1() { + throw a = x(); + } + function f2(a) { + throw a = x(); + } + function f3() { + var a; + throw a = x(); + } + function f4() { + try { + throw a = x(); + } catch (b) { + console.log(a); + } + } + function f5(a) { + try { + throw a = x(); + } catch (b) { + console.log(a); + } + } + function f6() { + var a; + try { + throw a = x(); + } catch (b) { + console.log(a); + } + } + function f7() { + try { + throw a = x(); + } finally { + console.log(a); + } + } + function f8(a) { + try { + throw a = x(); + } finally { + console.log(a); + } + } + function f9() { + var a; + try { + throw a = x(); + } finally { + console.log(a); + } + } + } + expect: { + function f1() { + throw a = x(); + } + function f2(a) { + throw x(); + } + function f3() { + throw x(); + } + function f4() { + try { + throw a = x(); + } catch (b) { + console.log(a); + } + } + function f5(a) { + try { + throw a = x(); + } catch (b) { + console.log(a); + } + } + function f6() { + var a; + try { + throw a = x(); + } catch (b) { + console.log(a); + } + } + function f7() { + try { + throw a = x(); + } finally { + console.log(a); + } + } + function f8(a) { + try { + throw a = x(); + } finally { + console.log(a); + } + } + function f9() { + var a; + try { + throw a = x(); + } finally { + console.log(a); + } + } } } From d18979bb23358fbdb153568109a5b564cd984920 Mon Sep 17 00:00:00 2001 From: kzc Date: Wed, 13 Dec 2017 23:12:06 -0500 Subject: [PATCH 14/42] improve `dead_code` tests (#2589) for #2588 --- test/compress/dead-code.js | 136 +++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 9612b92b..2d2f9d92 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -505,6 +505,30 @@ return_assignment: { console.log(a); } } + function y() { + console.log("y"); + } + function test(inc) { + var counter = 0; + x = function() { + counter += inc; + if (inc < 0) throw counter; + return counter; + }; + [ f1, f2, f3, f4, f5, f6 ].forEach(function(f, i) { + e = null; + try { + i += 1; + console.log("result " + f(10 * i, 100 * i, 1000 * i)); + } catch (x) { + console.log("caught " + x); + } + if (null !== e) console.log("e: " + e); + }); + } + var x, e; + test(1); + test(-1); } expect: { function f1(a, b, c) { @@ -533,7 +557,50 @@ return_assignment: { console.log(a); } } + function y() { + console.log("y"); + } + function test(inc) { + var counter = 0; + x = function() { + counter += inc; + if (inc < 0) throw counter; + return counter; + }; + [ f1, f2, f3, f4, f5, f6 ].forEach(function(f, i) { + e = null; + try { + i += 1; + console.log("result " + f(10 * i, 100 * i, 1000 * i)); + } catch (x) { + console.log("caught " + x); + } + if (null !== e) console.log("e: " + e); + }); + } + var x, e; + test(1); + test(-1); } + expect_stdout: [ + "y", + "result 31", + "result 2", + "e: 2", + "result 3", + "result 4", + "result 5", + "6", + "result 6", + "caught -1", + "caught -2", + "caught -3", + "caught -4", + "50", + "result undefined", + "60", + "caught -6", + ] } throw_assignment: { @@ -596,6 +663,26 @@ throw_assignment: { console.log(a); } } + function test(inc) { + var counter = 0; + x = function() { + counter += inc; + if (inc < 0) throw counter; + return counter; + }; + [ f1, f2, f3, f4, f5, f6, f7, f8, f9 ].forEach(function(f, i) { + a = null; + try { + f(10 * (1 + i)); + } catch (x) { + console.log("caught " + x); + } + if (null !== a) console.log("a: " + a); + }); + } + var x, a; + test(1); + test(-1); } expect: { function f1() { @@ -651,5 +738,54 @@ throw_assignment: { console.log(a); } } + function test(inc) { + var counter = 0; + x = function() { + counter += inc; + if (inc < 0) throw counter; + return counter; + }; + [ f1, f2, f3, f4, f5, f6, f7, f8, f9 ].forEach(function(f, i) { + a = null; + try { + f(10 * (1 + i)); + } catch (x) { + console.log("caught " + x); + } + if (null !== a) console.log("a: " + a); + }); + } + var x, a; + test(1); + test(-1); } + expect_stdout: [ + "caught 1", + "a: 1", + "caught 2", + "caught 3", + "4", + "a: 4", + "5", + "6", + "7", + "caught 7", + "a: 7", + "8", + "caught 8", + "9", + "caught 9", + "caught -1", + "caught -2", + "caught -3", + "null", + "50", + "undefined", + "null", + "caught -7", + "80", + "caught -8", + "undefined", + "caught -9", + ] } From 738fd52bc46c043db4a0cd415671f54b392ee6ac Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 14 Dec 2017 15:31:35 +0800 Subject: [PATCH 15/42] improve `collapse_vars` (#2591) - handle single-use assignments other than `AST_VarDef` - scan `AST_Call` for candidates --- lib/compress.js | 8 +++++++- test/compress/collapse_vars.js | 22 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index e394e8b2..05e0e1aa 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1014,7 +1014,9 @@ merge(Compressor.prototype, { var replace_all = value_def; if (!replace_all && lhs instanceof AST_SymbolRef) { var def = lhs.definition(); - replace_all = def.references.length - def.replaced == 1; + if (def.references.length - def.replaced == (candidate instanceof AST_VarDef ? 1 : 2)) { + replace_all = true; + } } var side_effects = value_has_side_effects(candidate); var may_throw = candidate.may_throw(compressor); @@ -1097,6 +1099,9 @@ merge(Compressor.prototype, { if (expr instanceof AST_Assign && !expr.left.has_side_effects(compressor) || expr instanceof AST_Unary && (expr.operator == "++" || expr.operator == "--")) { candidates.push(expr); + } else if (expr instanceof AST_Call) { + extract_candidates(expr.expression); + expr.args.forEach(extract_candidates); } else if (expr instanceof AST_Case) { extract_candidates(expr.expression); } else if (expr instanceof AST_Conditional) { @@ -1125,6 +1130,7 @@ merge(Compressor.prototype, { function find_stop(node, level) { var parent = scanner.parent(level); + 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_Exit) return node; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index bd1362b1..39d6b641 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1993,10 +1993,8 @@ undeclared: { } expect: { function f(x, y) { - var a; - a = x; b = y; - return b + a; + return b + x; } } } @@ -3978,3 +3976,21 @@ cascade_switch: { } } } + +cascade_call: { + options = { + collapse_vars: true, + unused: true, + } + input: { + function f(a) { + var b; + return x((b = a, y(b))); + } + } + expect: { + function f(a) { + return x(y(a)); + } + } +} From 02a6ce07eba11223518a22bce18e209371f78f39 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 14 Dec 2017 15:32:13 +0800 Subject: [PATCH 16/42] improve `reduce_vars` (#2592) - account for hoisting nature of `var` --- lib/compress.js | 11 +++++--- test/compress/reduce_vars.js | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 05e0e1aa..4166909f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -381,7 +381,10 @@ merge(Compressor.prototype, { && node.operator == "=" && node.left instanceof AST_SymbolRef) { var d = node.left.definition(); - if (safe_to_assign(d, node.right)) { + if (safe_to_assign(d, node.right) + || d.fixed === undefined && all(d.orig, function(sym) { + return sym instanceof AST_SymbolVar; + })) { d.references.push(node.left); d.fixed = function() { return node.right; @@ -561,9 +564,9 @@ merge(Compressor.prototype, { if (!safe_to_read(def)) return false; if (def.fixed === false) return false; if (def.fixed != null && (!value || def.references.length > 0)) return false; - return !def.orig.some(function(sym) { - return sym instanceof AST_SymbolDefun - || sym instanceof AST_SymbolLambda; + return all(def.orig, function(sym) { + return !(sym instanceof AST_SymbolDefun + || sym instanceof AST_SymbolLambda); }); } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 504ce6f0..108dc0e9 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -4784,3 +4784,57 @@ escape_local_throw: { } expect_stdout: "PASS" } + +inverted_var: { + options = { + evaluate: true, + inline: true, + passes: 3, + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + console.log(function() { + var a = 1; + return a; + }(), function() { + var b; + b = 2; + return b; + }(), function() { + c = 3; + return c; + var c; + }(), function(c) { + c = 4; + return c; + }(), function (c) { + c = 5; + return c; + var c; + }(), function c() { + c = 6; + return c; + }(), function c() { + c = 7; + return c; + var c; + }(), function() { + c = 8; + return c; + var c = "foo"; + }()); + } + expect: { + console.log(1, 2, 3, 4, 5, function c() { + c = 6; + return c; + }(), 7, function() { + c = 8; + return c; + var c = "foo"; + }()); + } + expect_stdout: true +} From 3f18a61532a86f0f52edbb50ca7d81e0869ad7c3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 14 Dec 2017 18:47:05 +0800 Subject: [PATCH 17/42] fix `reduce_vars` on single `AST_Defun` reference across loop (#2593) --- lib/compress.js | 7 ++++++- test/compress/reduce_vars.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 4166909f..96ed24a5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -334,6 +334,11 @@ merge(Compressor.prototype, { if (node instanceof AST_SymbolRef) { var d = node.definition(); d.references.push(node); + if (d.references.length == 1 + && !d.fixed + && d.orig[0] instanceof AST_SymbolDefun) { + loop_ids[d.id] = in_loop; + } var value; if (d.fixed === undefined || !safe_to_read(d) || d.single_use == "m") { d.fixed = false; @@ -402,9 +407,9 @@ merge(Compressor.prototype, { d.fixed = false; } else { d.fixed = node; + d.single_use = ref_once(d); loop_ids[d.id] = in_loop; mark(d, true); - d.single_use = ref_once(d); } var save_ids = safe_ids; safe_ids = Object.create(null); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 108dc0e9..ff93079d 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -4838,3 +4838,31 @@ inverted_var: { } expect_stdout: true } + +defun_single_use_loop: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + for (var x, i = 2; --i >= 0; ) { + var y = x; + x = f; + console.log(x === y); + } + function f() {}; + } + expect: { + for (var x, i = 2; --i >= 0; ) { + var y = x; + x = f; + console.log(x === y); + } + function f() {}; + } + expect_stdout: [ + "false", + "true", + ] +} From 90313875f75f68fecfc23c6c6f96f921da730301 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 14 Dec 2017 19:24:54 +0800 Subject: [PATCH 18/42] inline single-use `function` across loop (#2594) --- lib/compress.js | 7 +++- test/compress/functions.js | 75 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 96ed24a5..9b0a5350 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3862,12 +3862,15 @@ merge(Compressor.prototype, { } } if (fn instanceof AST_Function) { + var def; if (compressor.option("inline") - && exp === fn - && !fn.name && !fn.uses_arguments && !fn.uses_eval && fn.body.length == 1 + && (exp === fn ? !fn.name + : compressor.option("unused") + && (def = exp.definition()).references.length == 1 + && !recursive_ref(compressor, def)) && !fn.contains_this() && all(fn.argnames, function(arg) { return arg.__unused; diff --git a/test/compress/functions.js b/test/compress/functions.js index 7e87692d..c4281d5c 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -672,3 +672,78 @@ empty_body: { } } } + +inline_loop_1: { + options = { + inline: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + return x(); + } + for (;;) f(); + } + expect: { + for (;;) x(); + } +} + +inline_loop_2: { + options = { + inline: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + for (;;) f(); + function f() { + return x(); + } + } + expect: { + for (;;) x(); + } +} + +inline_loop_3: { + options = { + inline: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var f = function() { + return x(); + }; + for (;;) f(); + } + expect: { + for (;;) x(); + } +} + +inline_loop_4: { + options = { + inline: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + for (;;) f(); + var f = function() { + return x(); + }; + } + expect: { + for (;;) f(); + var f = function() { + return x(); + }; + } +} From 8f681b1d1721e931852be48720d26ba052eac96c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 15 Dec 2017 13:28:30 +0800 Subject: [PATCH 19/42] handle `inline` of function arguments (#2590) fixes #2476 --- lib/compress.js | 92 ++++++++++++++++++++++++++-------- test/compress/collapse_vars.js | 48 ++++++++---------- test/compress/functions.js | 39 +++++++++++--- test/compress/hoist_props.js | 8 +-- test/compress/reduce_vars.js | 13 ++--- test/mocha/glob.js | 2 +- 6 files changed, 137 insertions(+), 65 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 9b0a5350..aaadcd10 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2997,10 +2997,10 @@ merge(Compressor.prototype, { return self; }); - AST_Scope.DEFMETHOD("make_var_name", function(prefix) { - var var_names = this.var_names; + AST_Scope.DEFMETHOD("var_names", function() { + var var_names = this._var_names; if (!var_names) { - this.var_names = var_names = Object.create(null); + this._var_names = var_names = Object.create(null); this.enclosed.forEach(function(def) { var_names[def.name] = true; }); @@ -3008,6 +3008,11 @@ merge(Compressor.prototype, { var_names[name] = true; }); } + return var_names; + }); + + AST_Scope.DEFMETHOD("make_var_name", function(prefix) { + var var_names = this.var_names(); prefix = prefix.replace(/[^a-z_$]+/ig, "_"); var name = prefix; for (var i = 0; var_names[name]; i++) name = prefix + "$" + i; @@ -3862,7 +3867,7 @@ merge(Compressor.prototype, { } } if (fn instanceof AST_Function) { - var def; + var def, scope, value; if (compressor.option("inline") && !fn.uses_arguments && !fn.uses_eval @@ -3871,24 +3876,13 @@ merge(Compressor.prototype, { : compressor.option("unused") && (def = exp.definition()).references.length == 1 && !recursive_ref(compressor, def)) + && !self.has_pure_annotation(compressor) && !fn.contains_this() - && all(fn.argnames, function(arg) { - return arg.__unused; - }) - && !self.has_pure_annotation(compressor)) { - var value; - if (stat instanceof AST_Return) { - value = stat.value; - } else if (stat instanceof AST_SimpleStatement) { - value = make_node(AST_UnaryPrefix, stat, { - operator: "void", - expression: stat.body - }); - } - if (value) { - var args = self.args.concat(value); - return make_sequence(self, args).optimize(compressor); - } + && (scope = can_flatten_args(fn)) + && (value = flatten_body(stat))) { + var expressions = flatten_args(fn, scope); + expressions.push(value); + return make_sequence(self, expressions).optimize(compressor); } if (compressor.option("side_effects") && all(fn.body, is_empty)) { var args = self.args.concat(make_node(AST_Undefined, self)); @@ -3917,6 +3911,62 @@ merge(Compressor.prototype, { return best_of(compressor, ev, self); } return self; + + function can_flatten_args(fn) { + var scope = compressor.find_parent(AST_Scope); + var safe_to_inject = compressor.toplevel.vars || !(scope instanceof AST_Toplevel); + return all(fn.argnames, function(arg) { + return arg.__unused || safe_to_inject && !scope.var_names()[arg.name]; + }) && scope; + } + + function flatten_args(fn, scope) { + var decls = []; + var expressions = []; + for (var len = fn.argnames.length, i = len; --i >= 0;) { + var name = fn.argnames[i]; + var value = self.args[i]; + if (name.__unused) { + if (value || expressions.length) { + expressions.unshift(value || make_node(AST_Undefined, self)); + } + } else { + decls.unshift(make_node(AST_VarDef, name, { + name: name, + value: null + })); + var sym = make_node(AST_SymbolRef, name, name); + name.definition().references.push(sym); + expressions.unshift(make_node(AST_Assign, self, { + operator: "=", + left: sym, + right: value || make_node(AST_Undefined, self) + })); + } + } + for (i = len, len = self.args.length; i < len; i++) { + expressions.push(self.args[i]); + } + if (decls.length) { + for (i = 0; compressor.parent(i) !== scope;) i++; + i = scope.body.indexOf(compressor.parent(i - 1)) + 1; + scope.body.splice(i, 0, make_node(AST_Var, fn, { + definitions: decls + })); + } + return expressions; + } + + function flatten_body(stat) { + if (stat instanceof AST_Return) { + return stat.value; + } else if (stat instanceof AST_SimpleStatement) { + return make_node(AST_UnaryPrefix, stat, { + operator: "void", + expression: stat.body + }); + } + } }); OPT(AST_New, function(self, compressor){ diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 39d6b641..1a487981 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -3317,15 +3317,14 @@ issue_2436_4: { }(o)); } expect: { - console.log(function(c) { - return { - x: c.a, - y: c.b, - }; - }({ + console.log({ + x: (c = { a: 1, b: 2, - })); + }).a, + y: c.b, + }); + var c; } expect_stdout: true } @@ -3448,12 +3447,11 @@ issue_2436_8: { }(o)); } expect: { - console.log(function(c) { - return { - x: c.a, - y: c.b, - }; - }(o)); + console.log({ + x: (c = o).a, + y: c.b, + }); + var c; } expect_stdout: true } @@ -3478,12 +3476,11 @@ issue_2436_9: { } expect: { var o = console; - console.log(function(c) { - return { - x: c.a, - y: c.b, - }; - }(o)); + console.log({ + x: (c = o).a, + y: c.b, + }); + var c; } expect_stdout: true } @@ -3523,13 +3520,12 @@ issue_2436_10: { o = { b: 3 }; return n; } - console.log(function(c) { - return [ - c.a, - f(c.b), - c.b, - ]; - }(o).join(" ")); + console.log((c = o, [ + c.a, + f(c.b), + c.b, + ]).join(" ")); + var c; } expect_stdout: "1 2 2" } diff --git a/test/compress/functions.js b/test/compress/functions.js index c4281d5c..a36509af 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -578,11 +578,10 @@ issue_2531_1: { } expect: { function outer() { - return function(value) { - return function() { - return value; - }; - }("Hello"); + return value = "Hello", function() { + return value; + }; + var value; } console.log("Greeting:", outer()()); } @@ -593,9 +592,10 @@ issue_2531_2: { options = { evaluate: true, inline: true, - passes: 2, + passes: 3, reduce_funcs: true, reduce_vars: true, + side_effects: true, unused: true, } input: { @@ -627,9 +627,10 @@ issue_2531_3: { options = { evaluate: true, inline: true, - passes: 2, + passes: 3, reduce_funcs: true, reduce_vars: true, + side_effects: true, toplevel: true, unused: true, } @@ -747,3 +748,27 @@ inline_loop_4: { }; } } + +issue_2476: { + options = { + inline: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function foo(x, y, z) { + return x < y ? x * y + z : x * z - y; + } + for (var sum = 0, i = 0; i < 10; i++) + sum += foo(i, i + 1, 3 * i); + console.log(sum); + } + expect: { + for (var sum = 0, i = 0; i < 10; i++) + sum += (x = i, y = i + 1, z = 3 * i, x < y ? x * y + z : x * z - y); + var x, y, z; + console.log(sum); + } + expect_stdout: "465" +} diff --git a/test/compress/hoist_props.js b/test/compress/hoist_props.js index a46033d5..012a3fca 100644 --- a/test/compress/hoist_props.js +++ b/test/compress/hoist_props.js @@ -55,9 +55,8 @@ issue_2377_2: { console.log(obj.foo, obj.cube(3)); } expect: { - console.log(1, function(x) { - return x * x * x; - }(3)); + console.log(1, (x = 3, x * x * x)); + var x; } expect_stdout: "1 27" } @@ -67,9 +66,10 @@ issue_2377_3: { evaluate: true, inline: true, hoist_props: true, - passes: 3, + passes: 4, reduce_funcs: true, reduce_vars: true, + side_effects: true, toplevel: true, unused: true, } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index ff93079d..9ed9c974 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1396,7 +1396,7 @@ defun_inline_3: { options = { evaluate: true, inline: true, - passes: 2, + passes: 3, reduce_funcs: true, reduce_vars: true, side_effects: true, @@ -2250,12 +2250,11 @@ redefine_farg_2: { console.log(f([]), g([]), h([])); } expect: { - console.log(function(a) { - return typeof a; - }([]), "number",function(a, b) { + console.log((a = [], typeof a), "number",function(a, b) { a = b; return typeof a; }([])); + var a; } expect_stdout: "object number undefined" } @@ -2266,7 +2265,7 @@ redefine_farg_3: { evaluate: true, inline: true, keep_fargs: false, - passes: 2, + passes: 3, reduce_funcs: true, reduce_vars: true, sequences: true, @@ -3107,6 +3106,7 @@ obj_var_2: { obj_arg_1: { options = { + collapse_vars: true, evaluate: true, inline: true, passes: 2, @@ -3138,9 +3138,10 @@ obj_arg_1: { obj_arg_2: { options = { + collapse_vars: true, evaluate: true, inline: true, - passes: 2, + passes: 3, properties: true, reduce_funcs: true, reduce_vars: true, diff --git a/test/mocha/glob.js b/test/mocha/glob.js index eb5d477d..b6f1e049 100644 --- a/test/mocha/glob.js +++ b/test/mocha/glob.js @@ -26,7 +26,7 @@ describe("bin/uglifyjs with input file globs", function() { }); }); it("bin/uglifyjs with multiple input file globs.", function(done) { - var command = uglifyjscmd + ' "test/input/issue-1242/???.es5" "test/input/issue-1242/*.js" -mc toplevel,passes=2'; + var command = uglifyjscmd + ' "test/input/issue-1242/???.es5" "test/input/issue-1242/*.js" -mc toplevel,passes=3'; exec(command, function(err, stdout) { if (err) throw err; From 092d9affb829768e652f14c82080e27893e1f022 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 15 Dec 2017 16:33:19 +0800 Subject: [PATCH 20/42] fix `reduce_vars` on `do...while` (#2596) --- lib/compress.js | 12 +++++++++++- test/compress/reduce_vars.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index aaadcd10..0162fa4e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -479,7 +479,17 @@ merge(Compressor.prototype, { } return true; } - if (node instanceof AST_DWLoop) { + if (node instanceof AST_Do) { + var saved_loop = in_loop; + in_loop = node; + push(); + node.body.walk(tw); + node.condition.walk(tw); + pop(); + in_loop = saved_loop; + return true; + } + if (node instanceof AST_While) { var saved_loop = in_loop; in_loop = node; push(); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 9ed9c974..394bb586 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -4867,3 +4867,35 @@ defun_single_use_loop: { "true", ] } + +do_while: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + function f(a) { + do { + (function() { + a && (c = "PASS"); + })(); + } while (a = 0); + } + var c = "FAIL"; + f(1); + console.log(c); + } + expect: { + function f(a) { + do { + (function() { + a && (c = "PASS"); + })(); + } while (a = 0); + } + var c = "FAIL"; + f(1); + console.log(c); + } + expect_stdout: "PASS" +} From 7d6907cb99bac1e835febe30494ebca4c1a671d3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 15 Dec 2017 19:41:28 +0800 Subject: [PATCH 21/42] fix `dead_code` on nested `try` (#2599) fixes #2597 --- lib/compress.js | 13 ++++++------- test/compress/dead-code.js | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 0162fa4e..a8bcf54f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4751,11 +4751,7 @@ merge(Compressor.prototype, { node = parent; parent = compressor.parent(level++); if (parent instanceof AST_Exit) { - var try_node = find_try(level); - if (try_node) { - if (try_node.bfinally) break; - if (parent instanceof AST_Throw && try_node.bcatch) break; - } + if (in_try(level, parent instanceof AST_Throw)) break; if (self.operator == "=") return self.right; return make_node(AST_Binary, self, { operator: self.operator.slice(0, -1), @@ -4787,11 +4783,14 @@ merge(Compressor.prototype, { } return self; - function find_try(level) { + function in_try(level, no_catch) { var scope = self.left.definition().scope; var parent; while ((parent = compressor.parent(level++)) !== scope) { - if (parent instanceof AST_Try) return parent; + if (parent instanceof AST_Try) { + if (parent.bfinally) return true; + if (no_catch && parent.bcatch) return true; + } } } }); diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 2d2f9d92..591dd3a9 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -789,3 +789,42 @@ throw_assignment: { "caught -9", ] } + +issue_2597: { + options = { + dead_code: true, + } + input: { + function f(b) { + try { + try { + throw "foo"; + } catch (e) { + return b = true; + } + } finally { + b && (a = "PASS"); + } + } + var a = "FAIL"; + f(); + console.log(a); + } + expect: { + function f(b) { + try { + try { + throw "foo"; + } catch (e) { + return b = true; + } + } finally { + b && (a = "PASS"); + } + } + var a = "FAIL"; + f(); + console.log(a); + } + expect_stdout: "PASS" +} From db902af4c6e7a8c7e7a690591a0cd6a0d611300f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 15 Dec 2017 19:48:14 +0800 Subject: [PATCH 22/42] fix escape analysis on `||` and `&&` (#2600) fixes #2598 --- lib/compress.js | 1 + test/compress/reduce_vars.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index a8bcf54f..0fac3259 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -674,6 +674,7 @@ merge(Compressor.prototype, { d.escaped = true; return; } else if (parent instanceof AST_Array + || parent instanceof AST_Binary && lazy_op(parent.operator) || parent instanceof AST_Conditional && node !== parent.condition || parent instanceof AST_Sequence && node === parent.tail_node()) { mark_escaped(d, scope, parent, parent, level + 1); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 394bb586..bf1155b7 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -4899,3 +4899,27 @@ do_while: { } expect_stdout: "PASS" } + +issue_2598: { + options = { + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() {} + function g(a) { + return a || f; + } + console.log(g(false) === g(null)); + } + expect: { + function f() {} + function g(a) { + return a || f; + } + console.log(g(false) === g(null)); + } + expect_stdout: "true" +} From 6c686ce59342c42b7fdcf54296e28c6c6517e6ab Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 16 Dec 2017 02:16:35 +0800 Subject: [PATCH 23/42] fix nested `inline` (#2602) fixes #2601 --- lib/compress.js | 1 + test/compress/functions.js | 75 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 0fac3259..8c17422d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3942,6 +3942,7 @@ merge(Compressor.prototype, { expressions.unshift(value || make_node(AST_Undefined, self)); } } else { + scope.var_names()[name.name] = true; decls.unshift(make_node(AST_VarDef, name, { name: name, value: null diff --git a/test/compress/functions.js b/test/compress/functions.js index a36509af..8e67bc32 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -772,3 +772,78 @@ issue_2476: { } expect_stdout: "465" } + +issue_2601_1: { + options = { + inline: true, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + var a = "FAIL"; + (function() { + function f(b) { + function g(b) { + b && b(); + } + g(); + (function() { + b && (a = "PASS"); + })(); + } + f("foo"); + })(); + console.log(a); + } + expect: { + var a = "FAIL"; + (function() { + b = "foo", + function(b) { + b && b(); + }(), + b && (a = "PASS"); + var b; + })(), + console.log(a); + } + expect_stdout: "PASS" +} + +issue_2601_2: { + rename = true + options = { + evaluate: true, + inline: true, + passes: 3, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + mangle = {} + input: { + var a = "FAIL"; + (function() { + function f(b) { + function g(b) { + b && b(); + } + g(); + (function() { + b && (a = "PASS"); + })(); + } + f("foo"); + })(); + console.log(a); + } + expect: { + var a = "FAIL"; + a = "PASS", + console.log(a); + } + expect_stdout: "PASS" +} From 21794c9b8d0102ed00a15d1e54314908c92e4a24 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 16 Dec 2017 15:21:09 +0800 Subject: [PATCH 24/42] account for `catch` variable when `inline` (#2605) fixes #2604 --- lib/compress.js | 14 ++++++- test/compress/functions.js | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 8c17422d..ebcae482 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3924,10 +3924,20 @@ merge(Compressor.prototype, { return self; function can_flatten_args(fn) { - var scope = compressor.find_parent(AST_Scope); + var scope, level = 0; + var catches = Object.create(null); + do { + scope = compressor.parent(level++); + if (scope instanceof AST_Catch) { + catches[scope.argname.name] = true; + } + } while (!(scope instanceof AST_Scope)); var safe_to_inject = compressor.toplevel.vars || !(scope instanceof AST_Toplevel); return all(fn.argnames, function(arg) { - return arg.__unused || safe_to_inject && !scope.var_names()[arg.name]; + return arg.__unused + || safe_to_inject + && !catches[arg.name] + && !scope.var_names()[arg.name]; }) && scope; } diff --git a/test/compress/functions.js b/test/compress/functions.js index 8e67bc32..5f7fba6b 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -847,3 +847,79 @@ issue_2601_2: { } expect_stdout: "PASS" } + +issue_2604_1: { + options = { + inline: true, + side_effects: true, + unused: true, + } + input: { + var a = "FAIL"; + (function() { + try { + throw 1; + } catch (b) { + (function f(b) { + b && b(); + })(); + b && (a = "PASS"); + } + })(); + console.log(a); + } + expect: { + var a = "FAIL"; + (function() { + try { + throw 1; + } catch (b) { + (function(b) { + b && b(); + })(); + b && (a = "PASS"); + } + })(); + console.log(a); + } + expect_stdout: "PASS" +} + +issue_2604_2: { + rename = true + options = { + evaluate: true, + inline: true, + passes: 3, + reduce_vars: true, + side_effects: true, + unused: true, + } + mangle = {} + input: { + var a = "FAIL"; + (function() { + try { + throw 1; + } catch (b) { + (function f(b) { + b && b(); + })(); + b && (a = "PASS"); + } + })(); + console.log(a); + } + expect: { + var a = "FAIL"; + (function() { + try { + throw 1; + } catch (o) { + o && (a = "PASS"); + } + })(); + console.log(a); + } + expect_stdout: "PASS" +} From 7918a50d52809a854d6808c7a97f87f8e256506e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 17 Dec 2017 23:01:08 +0800 Subject: [PATCH 25/42] improve `reset_opt_flags()` (#2610) --- lib/compress.js | 618 +++++++++++++++++++++++++----------------------- 1 file changed, 318 insertions(+), 300 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index ebcae482..bccd7630 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -293,14 +293,12 @@ merge(Compressor.prototype, { if (index >= 0) { node.body[index] = node.body[index].transform(tt); } - } - if (node instanceof AST_If) { + } else 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) { + } else if (node instanceof AST_With) { node.body = node.body.transform(tt); } return node; @@ -308,292 +306,10 @@ merge(Compressor.prototype, { self.transform(tt); }); - AST_Toplevel.DEFMETHOD("reset_opt_flags", function(compressor) { - var reduce_vars = compressor.option("reduce_vars"); - var unused = compressor.option("unused"); - // Stack of look-up tables to keep track of whether a `SymbolDef` has been - // properly assigned before use: - // - `push()` & `pop()` when visiting conditional branches - // - backup & restore via `save_ids` when visiting out-of-order sections - var safe_ids = Object.create(null); - var suppressor = new TreeWalker(function(node) { - if (!(node instanceof AST_Symbol)) return; - var d = node.definition(); - if (!d) return; - if (node instanceof AST_SymbolRef) d.references.push(node); - d.fixed = false; - }); - var in_loop = null; - var loop_ids = Object.create(null); - var tw = new TreeWalker(function(node, descend) { - node._squeezed = false; - node._optimized = false; - if (reduce_vars) { - if (node instanceof AST_Toplevel) node.globals.each(reset_def); - if (node instanceof AST_Scope) node.variables.each(reset_def); - if (node instanceof AST_SymbolRef) { - var d = node.definition(); - d.references.push(node); - if (d.references.length == 1 - && !d.fixed - && d.orig[0] instanceof AST_SymbolDefun) { - loop_ids[d.id] = in_loop; - } - var value; - if (d.fixed === undefined || !safe_to_read(d) || d.single_use == "m") { - d.fixed = false; - } else if (d.fixed) { - value = node.fixed_value(); - if (value && ref_once(d)) { - d.single_use = value instanceof AST_Lambda - || d.scope === node.scope && value.is_constant_expression(); - } else { - d.single_use = false; - } - if (is_modified(node, value, 0, is_immutable(value))) { - if (d.single_use) { - d.single_use = "m"; - } else { - d.fixed = false; - } - } - } - mark_escaped(d, node.scope, node, value, 0); - } - if (node instanceof AST_SymbolCatch) { - node.definition().fixed = false; - } - if (node instanceof AST_VarDef) { - var d = node.name.definition(); - if (d.fixed === undefined || safe_to_assign(d, node.value)) { - if (node.value) { - d.fixed = function() { - return node.value; - }; - loop_ids[d.id] = in_loop; - mark(d, false); - descend(); - } else { - d.fixed = null; - } - mark(d, true); - return true; - } else if (node.value) { - d.fixed = false; - } - } - if (node instanceof AST_Assign - && node.operator == "=" - && node.left instanceof AST_SymbolRef) { - var d = node.left.definition(); - if (safe_to_assign(d, node.right) - || d.fixed === undefined && all(d.orig, function(sym) { - return sym instanceof AST_SymbolVar; - })) { - d.references.push(node.left); - d.fixed = function() { - return node.right; - }; - mark(d, false); - node.right.walk(tw); - mark(d, true); - return true; - } - } - if (node instanceof AST_Defun) { - node.inlined = false; - var d = node.name.definition(); - if (compressor.exposed(d) || safe_to_read(d)) { - d.fixed = false; - } else { - d.fixed = node; - d.single_use = ref_once(d); - loop_ids[d.id] = in_loop; - mark(d, true); - } - var save_ids = safe_ids; - safe_ids = Object.create(null); - descend(); - safe_ids = save_ids; - return true; - } - if (node instanceof AST_Function) { - node.inlined = false; - push(); - var iife; - if (!node.name - && (iife = tw.parent()) instanceof AST_Call - && iife.expression === node) { - // Virtually turn IIFE parameters into variable definitions: - // (function(a,b) {...})(c,d) => (function() {var a=c,b=d; ...})() - // So existing transformation rules can work on them. - node.argnames.forEach(function(arg, i) { - var d = arg.definition(); - if (!node.uses_arguments && d.fixed === undefined) { - d.fixed = function() { - return iife.args[i] || make_node(AST_Undefined, iife); - }; - loop_ids[d.id] = in_loop; - mark(d, true); - } else { - d.fixed = false; - } - }); - } - descend(); - pop(); - return true; - } - if (node instanceof AST_Accessor) { - push(); - descend(); - pop(); - return true; - } - if (node instanceof AST_Binary && lazy_op(node.operator)) { - node.left.walk(tw); - push(); - node.right.walk(tw); - pop(); - return true; - } - if (node instanceof AST_Conditional) { - node.condition.walk(tw); - push(); - node.consequent.walk(tw); - pop(); - push(); - node.alternative.walk(tw); - pop(); - return true; - } - if (node instanceof AST_If) { - node.condition.walk(tw); - push(); - node.body.walk(tw); - pop(); - if (node.alternative) { - push(); - node.alternative.walk(tw); - pop(); - } - return true; - } - if (node instanceof AST_Do) { - var saved_loop = in_loop; - in_loop = node; - push(); - node.body.walk(tw); - node.condition.walk(tw); - pop(); - in_loop = saved_loop; - return true; - } - if (node instanceof AST_While) { - var saved_loop = in_loop; - in_loop = node; - push(); - node.condition.walk(tw); - node.body.walk(tw); - pop(); - in_loop = saved_loop; - return true; - } - if (node instanceof AST_LabeledStatement) { - push(); - node.body.walk(tw); - pop(); - return true; - } - if (node instanceof AST_For) { - if (node.init) node.init.walk(tw); - var saved_loop = in_loop; - in_loop = node; - if (node.condition) { - push(); - node.condition.walk(tw); - pop(); - } - push(); - node.body.walk(tw); - pop(); - if (node.step) { - push(); - node.step.walk(tw); - pop(); - } - in_loop = saved_loop; - return true; - } - if (node instanceof AST_ForIn) { - node.init.walk(suppressor); - node.object.walk(tw); - var saved_loop = in_loop; - in_loop = node; - push(); - node.body.walk(tw); - pop(); - in_loop = saved_loop; - return true; - } - if (node instanceof AST_Try) { - push(); - walk_body(node, tw); - pop(); - if (node.bcatch) { - push(); - node.bcatch.walk(tw); - pop(); - } - if (node.bfinally) node.bfinally.walk(tw); - return true; - } - if (node instanceof AST_SwitchBranch) { - push(); - descend(); - pop(); - return true; - } - } - }); - this.walk(tw); + (function(def){ + def(AST_Node, noop); - function mark(def, safe) { - safe_ids[def.id] = safe; - } - - function safe_to_read(def) { - if (safe_ids[def.id]) { - if (def.fixed == null) { - var orig = def.orig[0]; - if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; - def.fixed = make_node(AST_Undefined, orig); - } - return true; - } - return def.fixed instanceof AST_Defun; - } - - function safe_to_assign(def, value) { - if (!HOP(safe_ids, def.id)) return false; - if (!safe_to_read(def)) return false; - if (def.fixed === false) return false; - if (def.fixed != null && (!value || def.references.length > 0)) return false; - return all(def.orig, function(sym) { - return !(sym instanceof AST_SymbolDefun - || sym instanceof AST_SymbolLambda); - }); - } - - function push() { - safe_ids = Object.create(safe_ids); - } - - function pop() { - safe_ids = Object.getPrototypeOf(safe_ids); - } - - function reset_def(def) { + function reset_def(compressor, def) { def.direct_access = false; def.escaped = false; if (def.scope.uses_eval || def.scope.uses_with) { @@ -608,12 +324,53 @@ merge(Compressor.prototype, { def.single_use = undefined; } - function ref_once(def) { - return unused + function reset_variables(compressor, node) { + node.variables.each(function(def) { + reset_def(compressor, def); + }); + } + + function push(tw) { + tw.safe_ids = Object.create(tw.safe_ids); + } + + function pop(tw) { + tw.safe_ids = Object.getPrototypeOf(tw.safe_ids); + } + + function mark(tw, def, safe) { + tw.safe_ids[def.id] = safe; + } + + function safe_to_read(tw, def) { + if (tw.safe_ids[def.id]) { + if (def.fixed == null) { + var orig = def.orig[0]; + if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; + def.fixed = make_node(AST_Undefined, orig); + } + return true; + } + return def.fixed instanceof AST_Defun; + } + + function safe_to_assign(tw, def, value) { + if (!HOP(tw.safe_ids, def.id)) return false; + if (!safe_to_read(tw, def)) return false; + if (def.fixed === false) return false; + if (def.fixed != null && (!value || def.references.length > 0)) return false; + return all(def.orig, function(sym) { + return !(sym instanceof AST_SymbolDefun + || sym instanceof AST_SymbolLambda); + }); + } + + function ref_once(tw, compressor, def) { + return compressor.option("unused") && !def.scope.uses_eval && !def.scope.uses_with && def.references.length == 1 - && loop_ids[def.id] === in_loop; + && tw.loop_ids[def.id] === tw.in_loop; } function is_immutable(value) { @@ -642,7 +399,7 @@ merge(Compressor.prototype, { return value instanceof AST_SymbolRef && value.fixed_value() || value; } - function is_modified(node, value, level, immutable) { + function is_modified(tw, node, value, level, immutable) { var parent = tw.parent(level); if (is_lhs(node, parent) || !immutable @@ -652,16 +409,16 @@ merge(Compressor.prototype, { || !(parent instanceof AST_New) && value.contains_this())) { return true; } else if (parent instanceof AST_Array) { - return is_modified(parent, parent, level + 1); + return is_modified(tw, parent, parent, level + 1); } else if (parent instanceof AST_ObjectKeyVal && node === parent.value) { var obj = tw.parent(level + 1); - return is_modified(obj, obj, level + 2); + return is_modified(tw, obj, obj, level + 2); } else if (parent instanceof AST_PropAccess && parent.expression === node) { - return !immutable && is_modified(parent, read_property(value, parent.property), level + 1); + return !immutable && is_modified(tw, parent, read_property(value, parent.property), level + 1); } } - function mark_escaped(d, scope, node, value, level) { + function mark_escaped(tw, d, scope, node, value, level) { var parent = tw.parent(level); if (value) { if (value.is_constant()) return; @@ -677,17 +434,278 @@ merge(Compressor.prototype, { || parent instanceof AST_Binary && lazy_op(parent.operator) || parent instanceof AST_Conditional && node !== parent.condition || parent instanceof AST_Sequence && node === parent.tail_node()) { - mark_escaped(d, scope, parent, parent, level + 1); + mark_escaped(tw, d, scope, parent, parent, level + 1); } else if (parent instanceof AST_ObjectKeyVal && node === parent.value) { var obj = tw.parent(level + 1); - mark_escaped(d, scope, obj, obj, level + 2); + mark_escaped(tw, d, scope, obj, obj, level + 2); } else if (parent instanceof AST_PropAccess && node === parent.expression) { value = read_property(value, parent.property); - mark_escaped(d, scope, parent, value, level + 1); + mark_escaped(tw, d, scope, parent, value, level + 1); if (value) return; } if (level == 0) d.direct_access = true; } + + var suppressor = new TreeWalker(function(node) { + if (!(node instanceof AST_Symbol)) return; + var d = node.definition(); + if (!d) return; + if (node instanceof AST_SymbolRef) d.references.push(node); + d.fixed = false; + }); + def(AST_Accessor, function(tw, descend) { + push(tw); + descend(); + pop(tw); + return true; + }); + def(AST_Assign, function(tw) { + var node = this; + if (node.operator != "=" || !(node.left instanceof AST_SymbolRef)) return; + var d = node.left.definition(); + if (safe_to_assign(tw, d, node.right) + || d.fixed === undefined && all(d.orig, function(sym) { + return sym instanceof AST_SymbolVar; + })) { + d.references.push(node.left); + d.fixed = function() { + return 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; + this.left.walk(tw); + push(tw); + this.right.walk(tw); + pop(tw); + return true; + }); + def(AST_Conditional, function(tw) { + this.condition.walk(tw); + push(tw); + this.consequent.walk(tw); + pop(tw); + push(tw); + this.alternative.walk(tw); + pop(tw); + return true; + }); + def(AST_Defun, function(tw, descend, compressor) { + reset_variables(compressor, this); + this.inlined = false; + var d = this.name.definition(); + if (compressor.exposed(d) || safe_to_read(tw, d)) { + d.fixed = false; + } else { + d.fixed = this; + d.single_use = ref_once(tw, compressor, d); + tw.loop_ids[d.id] = tw.in_loop; + mark(tw, d, true); + } + var save_ids = tw.safe_ids; + tw.safe_ids = Object.create(null); + descend(); + tw.safe_ids = save_ids; + return true; + }); + def(AST_Do, function(tw) { + var saved_loop = tw.in_loop; + tw.in_loop = this; + push(tw); + this.body.walk(tw); + this.condition.walk(tw); + pop(tw); + tw.in_loop = saved_loop; + return true; + }); + def(AST_For, function(tw) { + if (this.init) this.init.walk(tw); + var saved_loop = tw.in_loop; + tw.in_loop = this; + if (this.condition) { + push(tw); + this.condition.walk(tw); + pop(tw); + } + push(tw); + this.body.walk(tw); + pop(tw); + if (this.step) { + push(tw); + this.step.walk(tw); + pop(tw); + } + tw.in_loop = saved_loop; + return true; + }); + def(AST_ForIn, function(tw) { + this.init.walk(suppressor); + this.object.walk(tw); + var saved_loop = tw.in_loop; + tw.in_loop = this; + push(tw); + this.body.walk(tw); + pop(tw); + tw.in_loop = saved_loop; + return true; + }); + def(AST_Function, function(tw, descend, compressor) { + var node = this; + reset_variables(compressor, node); + node.inlined = false; + push(tw); + var iife; + if (!node.name + && (iife = tw.parent()) instanceof AST_Call + && iife.expression === node) { + // Virtually turn IIFE parameters into variable definitions: + // (function(a,b) {...})(c,d) => (function() {var a=c,b=d; ...})() + // So existing transformation rules can work on them. + node.argnames.forEach(function(arg, i) { + var d = arg.definition(); + if (!node.uses_arguments && d.fixed === undefined) { + d.fixed = function() { + return iife.args[i] || make_node(AST_Undefined, iife); + }; + tw.loop_ids[d.id] = tw.in_loop; + mark(tw, d, true); + } else { + d.fixed = false; + } + }); + } + descend(); + pop(tw); + return true; + }); + def(AST_If, function(tw) { + this.condition.walk(tw); + push(tw); + this.body.walk(tw); + pop(tw); + if (this.alternative) { + push(tw); + this.alternative.walk(tw); + pop(tw); + } + return true; + }); + def(AST_LabeledStatement, function(tw) { + push(tw); + this.body.walk(tw); + pop(tw); + return true; + }); + def(AST_SwitchBranch, function(tw, descend) { + push(tw); + descend(); + pop(tw); + return true; + }); + def(AST_SymbolCatch, function() { + this.definition().fixed = false; + }); + def(AST_SymbolRef, function(tw, descend, compressor) { + var d = this.definition(); + d.references.push(this); + if (d.references.length == 1 + && !d.fixed + && d.orig[0] instanceof AST_SymbolDefun) { + tw.loop_ids[d.id] = tw.in_loop; + } + var value; + if (d.fixed === undefined || !safe_to_read(tw, d) || d.single_use == "m") { + d.fixed = false; + } else if (d.fixed) { + value = this.fixed_value(); + if (value && ref_once(tw, compressor, d)) { + d.single_use = value instanceof AST_Lambda + || d.scope === this.scope && value.is_constant_expression(); + } else { + d.single_use = false; + } + if (is_modified(tw, this, value, 0, is_immutable(value))) { + if (d.single_use) { + d.single_use = "m"; + } else { + d.fixed = false; + } + } + } + mark_escaped(tw, d, this.scope, this, value, 0); + }); + def(AST_Toplevel, function(tw, descend, compressor) { + this.globals.each(function(def) { + reset_def(compressor, def); + }); + reset_variables(compressor, this); + }); + def(AST_Try, function(tw) { + push(tw); + walk_body(this, tw); + pop(tw); + if (this.bcatch) { + push(tw); + this.bcatch.walk(tw); + pop(tw); + } + if (this.bfinally) this.bfinally.walk(tw); + return true; + }); + def(AST_VarDef, function(tw, descend) { + var node = this; + var d = node.name.definition(); + if (d.fixed === undefined || safe_to_assign(tw, d, node.value)) { + if (node.value) { + d.fixed = function() { + return node.value; + }; + tw.loop_ids[d.id] = tw.in_loop; + mark(tw, d, false); + descend(); + } else { + d.fixed = null; + } + mark(tw, d, true); + return true; + } else if (node.value) { + d.fixed = false; + } + }); + def(AST_While, function(tw) { + var saved_loop = tw.in_loop; + tw.in_loop = this; + push(tw); + this.condition.walk(tw); + this.body.walk(tw); + pop(tw); + tw.in_loop = saved_loop; + return true; + }); + })(function(node, func){ + node.DEFMETHOD("reduce_vars", func); + }); + + AST_Toplevel.DEFMETHOD("reset_opt_flags", function(compressor) { + var reduce_vars = compressor.option("reduce_vars"); + var tw = new TreeWalker(function(node, descend) { + node._squeezed = false; + node._optimized = false; + if (reduce_vars) return node.reduce_vars(tw, descend, compressor); + }); + // Stack of look-up tables to keep track of whether a `SymbolDef` has been + // properly assigned before use: + // - `push()` & `pop()` when visiting conditional branches + // - backup & restore via `save_ids` when visiting out-of-order sections + tw.safe_ids = Object.create(null); + tw.in_loop = null; + tw.loop_ids = Object.create(null); + this.walk(tw); }); AST_Symbol.DEFMETHOD("fixed_value", function() { From 5de369fa67059efe694700513b66bf473fd98e06 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 17 Dec 2017 23:12:23 +0800 Subject: [PATCH 26/42] export `parse()` (#2608) --- tools/exports.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/exports.js b/tools/exports.js index e08296f3..c09436ab 100644 --- a/tools/exports.js +++ b/tools/exports.js @@ -2,4 +2,5 @@ exports["Dictionary"] = Dictionary; exports["TreeWalker"] = TreeWalker; exports["TreeTransformer"] = TreeTransformer; exports["minify"] = minify; +exports["parse"] = parse; exports["_push_uniq"] = push_uniq; From b29fc8b27c7bf4f48ff354ea2b9d55f7f4b69f79 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 18 Dec 2017 03:00:05 +0800 Subject: [PATCH 27/42] improve transversal efficiency in `collapse_vars` (#2611) fixes #2603 --- lib/compress.js | 61 ++++++++++++++++++++++------------ test/compress/collapse_vars.js | 22 ++++++++++++ 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index bccd7630..52c30964 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -905,10 +905,18 @@ merge(Compressor.prototype, { 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; } @@ -918,13 +926,13 @@ merge(Compressor.prototype, { } // Skip nodes before `candidate` as quickly as possible if (!hit) { - if (node === candidate) { - hit = true; - stop_after = find_stop(node, 0); - if (stop_after === node) abort = true; - return node; - } - return; + if (node !== hit_stack[hit_index]) return node; + hit_index++; + if (hit_index < hit_stack.length) return; + hit = true; + stop_after = find_stop(node, 0); + if (stop_after === node) abort = true; + return node; } // Stop immediately if these node types are encountered var parent = scanner.parent(); @@ -1013,11 +1021,11 @@ merge(Compressor.prototype, { if (abort) return node; // Skip nodes before `candidate` as quickly as possible if (!hit) { - if (node === candidate) { - hit = true; - return node; - } - return; + if (node !== hit_stack[hit_index]) return node; + hit_index++; + if (hit_index < hit_stack.length) return; + hit = true; + return node; } // Replace variable when found if (node instanceof AST_SymbolRef @@ -1038,9 +1046,12 @@ merge(Compressor.prototype, { // var a = x(), b = undefined; if (stat_index == 0 && compressor.option("unused")) extract_args(); // Find collapsible assignments + var hit_stack = []; extract_candidates(statements[stat_index]); while (candidates.length > 0) { - var candidate = candidates.pop(); + hit_stack = candidates.pop(); + var hit_index = 0; + var candidate = hit_stack[hit_stack.length - 1]; var value_def = null; var stop_after = null; var lhs = get_lhs(candidate); @@ -1074,6 +1085,7 @@ merge(Compressor.prototype, { if (abort && def.references.length - def.replaced > replaced) replaced = false; else { abort = false; + hit_index = 0; hit = funarg; for (var i = stat_index; !abort && i < statements.length; i++) { statements[i].transform(multi_replacer); @@ -1124,18 +1136,24 @@ merge(Compressor.prototype, { }); arg.walk(tw); } - if (arg) candidates.unshift(make_node(AST_VarDef, sym, { + if (arg) candidates.unshift([ make_node(AST_VarDef, sym, { name: sym, value: arg - })); + }) ]); } } } function extract_candidates(expr) { - if (expr instanceof AST_Assign && !expr.left.has_side_effects(compressor) - || expr instanceof AST_Unary && (expr.operator == "++" || expr.operator == "--")) { - candidates.push(expr); + hit_stack.push(expr); + if (expr instanceof AST_Assign) { + if (!expr.left.has_side_effects(compressor)) { + candidates.push(hit_stack.slice()); + } + } else if (expr instanceof AST_Unary) { + if (expr.operator == "++" || expr.operator == "--") { + candidates.push(hit_stack.slice()); + } } else if (expr instanceof AST_Call) { extract_candidates(expr.expression); expr.args.forEach(extract_candidates); @@ -1146,9 +1164,7 @@ merge(Compressor.prototype, { extract_candidates(expr.consequent); extract_candidates(expr.alternative); } else if (expr instanceof AST_Definitions) { - expr.definitions.forEach(function(var_def) { - if (var_def.value) candidates.push(var_def); - }); + expr.definitions.forEach(extract_candidates); } else if (expr instanceof AST_Exit) { if (expr.value) extract_candidates(expr.value); } else if (expr instanceof AST_For) { @@ -1162,7 +1178,10 @@ merge(Compressor.prototype, { } else if (expr instanceof AST_Switch) { extract_candidates(expr.expression); expr.body.forEach(extract_candidates); + } else if (expr instanceof AST_VarDef) { + if (expr.value) candidates.push(hit_stack.slice()); } + hit_stack.pop(); } function find_stop(node, level) { diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 1a487981..9a20c559 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -3990,3 +3990,25 @@ cascade_call: { } } } + +replace_all_var: { + options = { + collapse_vars: true, + unused: true, + } + input: { + var a = "PASS"; + (function() { + var b = b || c && c[a = "FAIL"], c = a; + })(); + console.log(a); + } + expect: { + var a = "PASS"; + (function() { + var b = b || c && c[a = "FAIL"], c = a; + })(); + console.log(a); + } + expect_stdout: "PASS" +} From 0b0eac1d5dc6e1cc1e9bf3682871cafdda59066d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 18 Dec 2017 12:07:53 +0800 Subject: [PATCH 28/42] drop property assignment to constants (#2612) --- lib/compress.js | 15 ++++++++++++-- test/compress/properties.js | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 52c30964..735b4d2d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1708,6 +1708,11 @@ merge(Compressor.prototype, { return this.consequent._dot_throw(compressor) || this.alternative._dot_throw(compressor); }) + def(AST_Dot, function(compressor) { + if (!is_strict(compressor)) return false; + if (this.expression instanceof AST_Function && this.property == "prototype") return false; + return true; + }); def(AST_Sequence, function(compressor) { return this.tail_node()._dot_throw(compressor); }); @@ -3184,8 +3189,14 @@ merge(Compressor.prototype, { } }); def(AST_Assign, function(compressor){ - this.write_only = !this.left.has_side_effects(compressor); - return this; + var left = this.left; + if (left.has_side_effects(compressor)) return this; + this.write_only = true; + while (left instanceof AST_PropAccess) { + left = left.expression; + } + if (left instanceof AST_Symbol) return this; + return this.right.drop_side_effect_free(compressor); }); def(AST_Conditional, function(compressor){ var consequent = this.consequent.drop_side_effect_free(compressor); diff --git a/test/compress/properties.js b/test/compress/properties.js index 6d4c0281..16374ef8 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -1054,3 +1054,43 @@ issue_2513: { "undefined undefined", ] } + +const_prop_assign_strict: { + options = { + pure_getters: "strict", + side_effects: true, + } + input: { + function Simulator() { + /abc/.index = 1; + this._aircraft = []; + } + (function() {}).prototype.destroy = x(); + } + expect: { + function Simulator() { + this._aircraft = []; + } + x(); + } +} + +const_prop_assign_pure: { + options = { + pure_getters: true, + side_effects: true, + } + input: { + function Simulator() { + /abc/.index = 1; + this._aircraft = []; + } + (function() {}).prototype.destroy = x(); + } + expect: { + function Simulator() { + this._aircraft = []; + } + x(); + } +} From 8ddcbc39e617a3ce53a340303fd9ef3226ee0065 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 18 Dec 2017 16:23:39 +0800 Subject: [PATCH 29/42] compress `apply()` & `call()` of `function` (#2613) - `fn.apply(a, [ ... ])` => `fn.call(a, ...)` - `fn.call(a, ... )` => `a, fn(...)` where `fn` can be `function` literal or symbol reference linked through `reduce_vars` --- lib/compress.js | 28 +++++++++ test/compress/functions.js | 126 +++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 735b4d2d..af1195d4 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3857,6 +3857,34 @@ merge(Compressor.prototype, { } } break; + case "apply": + if (self.args.length == 2 && self.args[1] instanceof AST_Array) { + var args = self.args[1].elements.slice(); + args.unshift(self.args[0]); + return make_node(AST_Call, self, { + expression: make_node(AST_Dot, exp, { + expression: exp.expression, + property: "call" + }), + args: args + }).optimize(compressor); + } + break; + case "call": + var func = exp.expression; + if (func instanceof AST_SymbolRef) { + func = func.fixed_value(); + } + if (func instanceof AST_Function && !func.contains_this()) { + return make_sequence(this, [ + self.args[0], + make_node(AST_Call, self, { + expression: exp.expression, + args: self.args.slice(1) + }) + ]).optimize(compressor); + } + break; } } if (compressor.option("unsafe_Func") diff --git a/test/compress/functions.js b/test/compress/functions.js index 5f7fba6b..23ed22df 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -923,3 +923,129 @@ issue_2604_2: { } expect_stdout: "PASS" } + +unsafe_apply_1: { + options = { + inline: true, + passes: 2, + reduce_vars: true, + side_effects: true, + unsafe: true, + unused: true, + } + input: { + (function(a, b) { + console.log(a, b); + }).apply("foo", [ "bar" ]); + (function(a, b) { + console.log(this, a, b); + }).apply("foo", [ "bar" ]); + (function(a, b) { + console.log(a, b); + }).apply("foo", [ "bar" ], "baz"); + } + expect: { + console.log("bar", void 0); + (function(a, b) { + console.log(this, a, b); + }).call("foo", "bar"); + (function(a, b) { + console.log(a, b); + }).apply("foo", [ "bar" ], "baz"); + } + expect_stdout: true +} + +unsafe_apply_2: { + options = { + reduce_vars: true, + side_effects: true, + toplevel: true, + unsafe: true, + } + input: { + function foo() { + console.log(a, b); + } + var bar = function(a, b) { + console.log(this, a, b); + } + (function() { + foo.apply("foo", [ "bar" ]); + bar.apply("foo", [ "bar" ]); + })(); + } + expect: { + function foo() { + console.log(a, b); + } + var bar = function(a, b) { + console.log(this, a, b); + } + (function() { + foo("bar"); + bar.call("foo", "bar"); + })(); + } + expect_stdout: true +} + +unsafe_call_1: { + options = { + inline: true, + passes: 2, + reduce_vars: true, + side_effects: true, + unsafe: true, + unused: true, + } + input: { + (function(a, b) { + console.log(a, b); + }).call("foo", "bar"); + (function(a, b) { + console.log(this, a, b); + }).call("foo", "bar"); + } + expect: { + console.log("bar", void 0); + (function(a, b) { + console.log(this, a, b); + }).call("foo", "bar"); + } + expect_stdout: true +} + +unsafe_call_2: { + options = { + reduce_vars: true, + side_effects: true, + toplevel: true, + unsafe: true, + } + input: { + function foo() { + console.log(a, b); + } + var bar = function(a, b) { + console.log(this, a, b); + } + (function() { + foo.call("foo", "bar"); + bar.call("foo", "bar"); + })(); + } + expect: { + function foo() { + console.log(a, b); + } + var bar = function(a, b) { + console.log(this, a, b); + } + (function() { + foo("bar"); + bar.call("foo", "bar"); + })(); + } + expect_stdout: true +} From 4b334edf491bd4c43a72e1a08ad2cf360f240515 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 19 Dec 2017 03:05:30 +0800 Subject: [PATCH 30/42] handle global constant collision with local variable after `inline` (#2617) fixes #2616 --- lib/compress.js | 5 ++++- test/compress/functions.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index af1195d4..c51a90c1 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4028,13 +4028,16 @@ merge(Compressor.prototype, { expressions.unshift(value || make_node(AST_Undefined, self)); } } else { + var def = name.definition(); scope.var_names()[name.name] = true; + scope.variables.set(name.name, def); + scope.enclosed.push(def); decls.unshift(make_node(AST_VarDef, name, { name: name, value: null })); var sym = make_node(AST_SymbolRef, name, name); - name.definition().references.push(sym); + def.references.push(sym); expressions.unshift(make_node(AST_Assign, self, { operator: "=", left: sym, diff --git a/test/compress/functions.js b/test/compress/functions.js index 23ed22df..c8b7ed6f 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1049,3 +1049,37 @@ unsafe_call_2: { } expect_stdout: true } + +issue_2616: { + options = { + evaluate: true, + inline: true, + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + var c = "FAIL"; + (function() { + function f() { + function g(NaN) { + (true << NaN) - 0/0 || (c = "PASS"); + } + g([]); + } + f(); + })(); + console.log(c); + } + expect: { + var c = "FAIL"; + (function() { + (function() { + NaN = [], (true << NaN) - 0/0 || (c = "PASS"); + var NaN; + })(); + })(); + console.log(c); + } + expect_stdout: "PASS" +} From 032f096b7f39d12c303e4d0c096619ad3c9f9384 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 19 Dec 2017 05:22:05 +0800 Subject: [PATCH 31/42] add test for #2613 (#2618) --- test/compress/functions.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/compress/functions.js b/test/compress/functions.js index c8b7ed6f..41dfc6a9 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1050,6 +1050,24 @@ unsafe_call_2: { expect_stdout: true } +unsafe_call_3: { + options = { + side_effects: true, + unsafe: true, + } + input: { + console.log(function() { + return arguments[0] + eval("arguments")[1]; + }.call(0, 1, 2)); + } + expect: { + console.log(function() { + return arguments[0] + eval("arguments")[1]; + }(1, 2)); + } + expect_stdout: "3" +} + issue_2616: { options = { evaluate: true, From 01057cf76d799edc5c7dd45d42a2e7bf44589948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C5=A0pan=C4=9Bl?= Date: Tue, 19 Dec 2017 10:56:16 +0100 Subject: [PATCH 32/42] Transform can be simplified when clone is not done. (#2621) --- lib/transform.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 8008e571..dcde62c2 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -60,12 +60,9 @@ TreeTransformer.prototype = new TreeWalker; tw.push(this); if (tw.before) x = tw.before(this, descend, in_list); if (x === undefined) { - if (!tw.after) { - x = this; - descend(x, tw); - } else { - tw.stack[tw.stack.length - 1] = x = this; - descend(x, tw); + x = this; + descend(x, tw); + if (tw.after) { y = tw.after(x, in_list); if (y !== undefined) x = y; } From 2273655c17b41ab276172afecd652a297c550c00 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 19 Dec 2017 22:19:33 +0800 Subject: [PATCH 33/42] fix `inline` after single-use `reduce_vars` (#2623) --- lib/compress.js | 4 ++- test/compress/functions.js | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index c51a90c1..c95e4f84 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4004,7 +4004,9 @@ merge(Compressor.prototype, { var catches = Object.create(null); do { scope = compressor.parent(level++); - if (scope instanceof AST_Catch) { + if (scope instanceof AST_SymbolRef) { + scope = scope.fixed_value(); + } else if (scope instanceof AST_Catch) { catches[scope.argname.name] = true; } } while (!(scope instanceof AST_Scope)); diff --git a/test/compress/functions.js b/test/compress/functions.js index 41dfc6a9..bd65a11d 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1101,3 +1101,74 @@ issue_2616: { } expect_stdout: "PASS" } + +issue_2620_1: { + options = { + inline: true, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + var c = "FAIL"; + (function() { + function f(a) { + var b = function g(a) { + a && a(); + }(); + if (a) { + var d = c = "PASS"; + } + } + f(1); + })(); + console.log(c); + } + expect: { + var c = "FAIL"; + (function() { + (function(a) { + if (function(a) { + a && a(); + }(), a) c = "PASS"; + })(1); + })(), + console.log(c); + } + expect_stdout: "PASS" +} + +issue_2620_2: { + options = { + conditionals: true, + evaluate: true, + inline: true, + passes: 2, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + var c = "FAIL"; + (function() { + function f(a) { + var b = function g(a) { + a && a(); + }(); + if (a) { + var d = c = "PASS"; + } + } + f(1); + })(); + console.log(c); + } + expect: { + var c = "FAIL"; + c = "PASS", + console.log(c); + } + expect_stdout: "PASS" +} From fac003c64f5512692e67e41a55b21c74a32a3c6b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 20 Dec 2017 02:48:04 +0800 Subject: [PATCH 34/42] avoid `inline` of function with special argument names (#2625) --- lib/compress.js | 2 + test/compress/functions.js | 94 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index c95e4f84..5aacfd73 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -858,6 +858,7 @@ merge(Compressor.prototype, { || compressor.option("unsafe") && global_names(this.name); }); + var identifier_atom = makePredicate("Infinity NaN undefined"); function is_identifier_atom(node) { return node instanceof AST_Infinity || node instanceof AST_NaN @@ -4015,6 +4016,7 @@ merge(Compressor.prototype, { return arg.__unused || safe_to_inject && !catches[arg.name] + && !identifier_atom(arg.name) && !scope.var_names()[arg.name]; }) && scope; } diff --git a/test/compress/functions.js b/test/compress/functions.js index bd65a11d..4c22652e 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1092,10 +1092,9 @@ issue_2616: { expect: { var c = "FAIL"; (function() { - (function() { - NaN = [], (true << NaN) - 0/0 || (c = "PASS"); - var NaN; - })(); + !function(NaN) { + (true << NaN) - 0/0 || (c = "PASS"); + }([]); })(); console.log(c); } @@ -1172,3 +1171,90 @@ issue_2620_2: { } expect_stdout: "PASS" } + +issue_2620_3: { + options = { + evaluate: true, + inline: true, + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + var c = "FAIL"; + (function() { + function f(a, NaN) { + function g() { + switch (a) { + case a: + break; + case c = "PASS", NaN: + break; + } + } + g(); + } + f(0/0); + })(); + console.log(c); + } + expect: { + var c = "FAIL"; + (function() { + (function(a, NaN) { + (function() { + switch (a) { + case a: + break; + case c = "PASS", NaN: + break; + } + })(); + })(NaN); + })(); + console.log(c); + } + expect_stdout: "PASS" +} + +issue_2620_4: { + rename = true, + options = { + evaluate: true, + dead_code: true, + inline: true, + passes: 2, + reduce_vars: true, + side_effects: true, + switches: true, + unused: true, + } + input: { + var c = "FAIL"; + (function() { + function f(a, NaN) { + function g() { + switch (a) { + case a: + break; + case c = "PASS", NaN: + break; + } + } + g(); + } + f(0/0); + })(); + console.log(c); + } + expect: { + var c = "FAIL"; + !function() { + switch (NaN) { + case void (c = "PASS"): + } + }(); + console.log(c); + } + expect_stdout: "PASS" +} From 86ae5881b7b269dc656520ff4dddbbd365013a0b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 20 Dec 2017 17:05:40 +0800 Subject: [PATCH 35/42] disable `hoist_funs` by default (#2626) --- README.md | 2 +- lib/compress.js | 2 +- test/mocha/glob.js | 2 +- test/mocha/minify.js | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ef496178..43793dcd 100644 --- a/README.md +++ b/README.md @@ -627,7 +627,7 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `global_defs` (default: `{}`) -- see [conditional compilation](#conditional-compilation) -- `hoist_funs` (default: `true`) -- hoist function declarations +- `hoist_funs` (default: `false`) -- hoist function declarations - `hoist_props` (default: `true`) -- hoist properties from constant object and array literals into regular variables subject to a set of constraints. For example: diff --git a/lib/compress.js b/lib/compress.js index 5aacfd73..3410b5a7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -58,7 +58,7 @@ function Compressor(options, false_by_default) { evaluate : !false_by_default, expression : false, global_defs : {}, - hoist_funs : !false_by_default, + hoist_funs : false, hoist_props : !false_by_default, hoist_vars : false, ie8 : false, diff --git a/test/mocha/glob.js b/test/mocha/glob.js index b6f1e049..58c40cf0 100644 --- a/test/mocha/glob.js +++ b/test/mocha/glob.js @@ -11,7 +11,7 @@ describe("bin/uglifyjs with input file globs", function() { exec(command, function(err, stdout) { if (err) throw err; - assert.strictEqual(stdout, 'function foo(o){print("Foo:",2*o)}var print=console.log.bind(console);\n'); + assert.strictEqual(stdout, 'var print=console.log.bind(console);function foo(o){print("Foo:",2*o)}\n'); done(); }); }); diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 157d6515..5d9512f3 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -43,7 +43,7 @@ describe("minify", function() { compressed += result.code; }); assert.strictEqual(JSON.stringify(cache).slice(0, 20), '{"cname":5,"props":{'); - assert.strictEqual(compressed, 'function n(n){return 3*n}function r(n){return n/2}function c(o){l("Foo:",2*o)}var l=console.log.bind(console);var f=n(3),i=r(12);l("qux",f,i),c(11);'); + assert.strictEqual(compressed, 'function n(n){return 3*n}function r(n){return n/2}var c=console.log.bind(console);function l(o){c("Foo:",2*o)}var f=n(3),i=r(12);c("qux",f,i),l(11);'); assert.strictEqual(run_code(compressed), run_code(original)); }); @@ -69,7 +69,7 @@ describe("minify", function() { compressed += result.code; }); assert.strictEqual(JSON.stringify(cache).slice(0, 28), '{"vars":{"cname":5,"props":{'); - assert.strictEqual(compressed, 'function n(n){return 3*n}function r(n){return n/2}function c(o){l("Foo:",2*o)}var l=console.log.bind(console);var f=n(3),i=r(12);l("qux",f,i),c(11);'); + assert.strictEqual(compressed, 'function n(n){return 3*n}function r(n){return n/2}var c=console.log.bind(console);function l(o){c("Foo:",2*o)}var f=n(3),i=r(12);c("qux",f,i),l(11);'); assert.strictEqual(run_code(compressed), run_code(original)); }); From 7ac7b0872fd8ba6866162697ac6956a737e20a04 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 20 Dec 2017 17:05:53 +0800 Subject: [PATCH 36/42] remove AST hack from `inline` (#2627) --- lib/compress.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 3410b5a7..85cc6ddf 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4036,8 +4036,10 @@ merge(Compressor.prototype, { scope.var_names()[name.name] = true; scope.variables.set(name.name, def); scope.enclosed.push(def); + var symbol = make_node(AST_SymbolVar, name, name); + def.orig.push(symbol); decls.unshift(make_node(AST_VarDef, name, { - name: name, + name: symbol, value: null })); var sym = make_node(AST_SymbolRef, name, name); From 4113609dd4d782f0ceb9ec1c3e9c829e05a93aed Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 20 Dec 2017 23:52:18 +0800 Subject: [PATCH 37/42] extend `test/ufuzz.js` to `inline` & `reduce_funcs` (#2620) - forward call `fN()` - allow forward call functions to be single-use - avoid generating `AST_Defun` within blocks --- test/ufuzz.js | 55 +++++++++++++++++++++++++++++-------------------- test/ufuzz.json | 5 +---- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index 578103e8..14b8f114 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -263,10 +263,8 @@ var CAN_CONTINUE = true; var CANNOT_CONTINUE = false; var CAN_RETURN = false; var CANNOT_RETURN = true; -var NOT_GLOBAL = true; -var IN_GLOBAL = true; -var ANY_TYPE = false; -var NO_DECL = true; +var NO_DEFUN = false; +var DEFUN_OK = true; var DONT_STORE = true; var VAR_NAMES = [ @@ -307,6 +305,7 @@ var TYPEOF_OUTCOMES = [ var unique_vars = []; var loops = 0; var funcs = 0; +var called = Object.create(null); var labels = 10000; function rng(max) { @@ -323,21 +322,22 @@ function createTopLevelCode() { unique_vars.length = 0; loops = 0; funcs = 0; + called = Object.create(null); return [ strictMode(), - 'var a = 100, b = 10, c = 0;', + 'var _calls_ = 10, a = 100, b = 10, c = 0;', rng(2) == 0 ? createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0) - : createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, IN_GLOBAL, ANY_TYPE, CANNOT_THROW, 0), + : createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, DEFUN_OK, CANNOT_THROW, 0), 'console.log(null, a, b, c);' // preceding `null` makes for a cleaner output (empty string still shows up etc) ].join('\n'); } -function createFunctions(n, recurmax, inGlobal, noDecl, canThrow, stmtDepth) { +function createFunctions(n, recurmax, allowDefun, canThrow, stmtDepth) { if (--recurmax < 0) { return ';'; } var s = ''; while (n-- > 0) { - s += createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) + '\n'; + s += createFunction(recurmax, allowDefun, canThrow, stmtDepth) + '\n'; } return s; } @@ -363,16 +363,16 @@ function filterDirective(s) { return s; } -function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { +function createFunction(recurmax, allowDefun, canThrow, stmtDepth) { if (--recurmax < 0) { return ';'; } if (!STMT_COUNT_FROM_GLOBAL) stmtDepth = 0; - var func = funcs++; var namesLenBefore = VAR_NAMES.length; var name; - if (inGlobal || rng(5) > 0) name = 'f' + func; - else { + if (allowDefun || rng(5) > 0) { + name = 'f' + funcs++; + } else { unique_vars.push('a', 'b', 'c'); - name = createVarName(MANDATORY, noDecl); + name = createVarName(MANDATORY, !allowDefun); unique_vars.length -= 3; } var s = [ @@ -381,7 +381,7 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { ]; if (rng(5) === 0) { // functions with functions. lower the recursion to prevent a mess. - s.push(createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth)); + s.push(createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), DEFUN_OK, canThrow, stmtDepth)); } else { // functions with statements s.push(createStatements(3, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth)); @@ -391,12 +391,16 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { VAR_NAMES.length = namesLenBefore; - if (noDecl) s = 'var ' + createVarName(MANDATORY) + ' = ' + s; - // avoid "function statements" (decl inside statements) - else if (inGlobal || rng(10) > 0) s += 'var ' + createVarName(MANDATORY) + ' = ' + name; - s += '(' + createArgs(recurmax, stmtDepth, canThrow) + ');'; + if (!allowDefun) { + // avoid "function statements" (decl inside statements) + s = 'var ' + createVarName(MANDATORY) + ' = ' + s; + s += '(' + createArgs(recurmax, stmtDepth, canThrow) + ')'; + } else if (!(name in called) || rng(3) > 0) { + s += 'var ' + createVarName(MANDATORY) + ' = ' + name; + s += '(' + createArgs(recurmax, stmtDepth, canThrow) + ')'; + } - return s; + return s + ';'; } function createStatements(n, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { @@ -541,7 +545,7 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn case STMT_FUNC_EXPR: // "In non-strict mode code, functions can only be declared at top level, inside a block, or ..." // (dont both with func decls in `if`; it's only a parser thing because you cant call them without a block) - return '{' + createFunction(recurmax, NOT_GLOBAL, NO_DECL, canThrow, stmtDepth) + '}'; + return '{' + createFunction(recurmax, NO_DEFUN, canThrow, stmtDepth) + '}'; case STMT_TRY: // catch var could cause some problems // note: the "blocks" are syntactically mandatory for try/catch/finally @@ -648,7 +652,7 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { '(function ' + name + '(){', strictMode(), createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), - '})()' + rng(2) == 0 ? '})' : '})()' ); break; case 1: @@ -687,7 +691,7 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { } s.push( createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), - '}' + rng(2) == 0 ? '}' : '}()' ); break; } @@ -754,6 +758,13 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { case p++: var name = getVarName(); return name + ' && ' + name + '.' + getDotKey(); + case p++: + case p++: + case p++: + case p++: + var name = rng(3) == 0 ? getVarName() : 'f' + rng(funcs + 2); + called[name] = true; + return 'typeof ' + name + ' == "function" && --_calls_ >= 0 && ' + name + '(' + createArgs(recurmax, stmtDepth, canThrow) + ')'; } _createExpression.N = p; return _createExpression(recurmax, noComma, stmtDepth, canThrow); diff --git a/test/ufuzz.json b/test/ufuzz.json index 0d737d31..4057a351 100644 --- a/test/ufuzz.json +++ b/test/ufuzz.json @@ -15,15 +15,12 @@ }, {}, { - "compress": { - "hoist_props": true - }, "toplevel": true }, { "compress": { "keep_fargs": false, - "passes": 3 + "passes": 100 } } ] From edb4e3bd52e1623425927a7d63963ba3b87a3ec2 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 22 Dec 2017 04:59:54 +0800 Subject: [PATCH 38/42] make comments output more robust (#2633) - improve handling of comments right after `return` - retain comments after `OutputStream` - preserve trailing comments - fix handling of new line before comments - handle comments around parentheses fixes #88 fixes #112 fixes #218 fixes #372 fixes #2629 --- lib/ast.js | 2 +- lib/output.js | 212 ++++++++++++++++++++++------------- lib/parse.js | 15 ++- lib/utils.js | 2 +- test/compress/pure_funcs.js | 82 ++++++++++++++ test/mocha/comment-filter.js | 6 +- test/mocha/comment.js | 172 ++++++++++++++++++++++++++++ 7 files changed, 401 insertions(+), 90 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 997486c2..65918675 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -87,7 +87,7 @@ function DEFNODE(type, props, methods, base) { return ctor; }; -var AST_Token = DEFNODE("Token", "type value line col pos endline endcol endpos nlb comments_before file raw", { +var AST_Token = DEFNODE("Token", "type value line col pos endline endcol endpos nlb comments_before comments_after file raw", { }, null); var AST_Node = DEFNODE("Node", "start end", { diff --git a/lib/output.js b/lib/output.js index a4c41f11..b0cecf06 100644 --- a/lib/output.js +++ b/lib/output.js @@ -52,6 +52,7 @@ function is_some_comments(comment) { function OutputStream(options) { + var readonly = !options; options = defaults(options, { ascii_only : false, beautify : false, @@ -199,6 +200,7 @@ function OutputStream(options) { var might_need_space = false; var might_need_semicolon = false; var might_add_newline = 0; + var need_newline_indented = false; var last = ""; var mapping_token, mapping_name, mappings = options.source_map && []; @@ -257,6 +259,13 @@ function OutputStream(options) { function print(str) { str = String(str); var ch = str.charAt(0); + if (need_newline_indented && ch) { + need_newline_indented = false; + if (ch != "\n") { + print("\n"); + indent(); + } + } var prev = last.charAt(last.length - 1); if (might_need_semicolon) { might_need_semicolon = false; @@ -427,6 +436,109 @@ function OutputStream(options) { return OUTPUT; }; + function prepend_comments(node) { + var self = this; + var start = node.start; + if (!(start.comments_before && start.comments_before._dumped === self)) { + var comments = start.comments_before; + if (!comments) { + comments = start.comments_before = []; + } + comments._dumped = self; + + if (node instanceof AST_Exit && node.value) { + var tw = new TreeWalker(function(node) { + var parent = tw.parent(); + if (parent instanceof AST_Exit + || parent instanceof AST_Binary && parent.left === node + || parent.TYPE == "Call" && parent.expression === node + || parent instanceof AST_Conditional && parent.condition === node + || parent instanceof AST_Dot && parent.expression === node + || parent instanceof AST_Sequence && parent.expressions[0] === node + || parent instanceof AST_Sub && parent.expression === node + || parent instanceof AST_UnaryPostfix) { + var text = node.start.comments_before; + if (text && text._dumped !== self) { + text._dumped = self; + comments = comments.concat(text); + } + } else { + return true; + } + }); + tw.push(node); + node.value.walk(tw); + } + + if (current_pos == 0) { + if (comments.length > 0 && options.shebang && comments[0].type == "comment5") { + print("#!" + comments.shift().value + "\n"); + indent(); + } + var preamble = options.preamble; + if (preamble) { + print(preamble.replace(/\r\n?|[\n\u2028\u2029]|\s*$/g, "\n")); + } + } + + comments = comments.filter(comment_filter, node); + if (comments.length == 0) return; + var last_nlb = /(^|\n) *$/.test(OUTPUT); + comments.forEach(function(c, i) { + if (!last_nlb) { + if (c.nlb) { + print("\n"); + indent(); + last_nlb = true; + } else if (i > 0) { + space(); + } + } + if (/comment[134]/.test(c.type)) { + print("//" + c.value + "\n"); + indent(); + last_nlb = true; + } else if (c.type == "comment2") { + print("/*" + c.value + "*/"); + last_nlb = false; + } + }); + if (!last_nlb) { + if (start.nlb) { + print("\n"); + indent(); + } else { + space(); + } + } + } + } + + function append_comments(node, tail) { + var self = this; + var token = node.end; + if (!token) return; + var comments = token[tail ? "comments_before" : "comments_after"]; + if (comments && comments._dumped !== self) { + comments._dumped = self; + comments.filter(comment_filter, node).forEach(function(c, i) { + if (need_newline_indented || c.nlb) { + print("\n"); + indent(); + need_newline_indented = false; + } else if (i > 0 || !tail) { + space(); + } + if (/comment[134]/.test(c.type)) { + print("//" + c.value); + need_newline_indented = true; + } else if (c.type == "comment2") { + print("/*" + c.value + "*/"); + } + }); + } + } + var stack = []; return { get : get, @@ -464,7 +576,8 @@ function OutputStream(options) { with_square : with_square, add_mapping : add_mapping, option : function(opt) { return options[opt] }, - comment_filter : comment_filter, + prepend_comments: readonly ? noop : prepend_comments, + append_comments : readonly ? noop : append_comments, line : function() { return current_line }, col : function() { return current_col }, pos : function() { return current_pos }, @@ -500,9 +613,10 @@ function OutputStream(options) { use_asm = active_scope; } function doit() { - self.add_comments(stream); + stream.prepend_comments(self); self.add_source_map(stream); generator(self, stream); + stream.append_comments(self); } stream.push_node(self); if (force_parens || self.needs_parens(stream)) { @@ -519,77 +633,10 @@ function OutputStream(options) { AST_Node.DEFMETHOD("print_to_string", function(options){ var s = OutputStream(options); - if (!options) s._readonly = true; this.print(s); return s.get(); }); - /* -----[ comments ]----- */ - - AST_Node.DEFMETHOD("add_comments", function(output){ - if (output._readonly) return; - var self = this; - var start = self.start; - if (start && !start._comments_dumped) { - start._comments_dumped = true; - var comments = start.comments_before || []; - - // XXX: ugly fix for https://github.com/mishoo/UglifyJS2/issues/112 - // and https://github.com/mishoo/UglifyJS2/issues/372 - if (self instanceof AST_Exit && self.value) { - self.value.walk(new TreeWalker(function(node){ - if (node.start && node.start.comments_before) { - comments = comments.concat(node.start.comments_before); - node.start.comments_before = []; - } - if (node instanceof AST_Function || - node instanceof AST_Array || - node instanceof AST_Object) - { - return true; // don't go inside. - } - })); - } - - if (output.pos() == 0) { - if (comments.length > 0 && 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 - if (!output.option("beautify") && comments.length > 0 && - /comment[134]/.test(comments[0].type) && - output.col() !== 0 && comments[0].nlb) - { - output.print("\n"); - } - - comments.forEach(function(c){ - if (/comment[134]/.test(c.type)) { - output.print("//" + c.value + "\n"); - output.indent(); - } - else if (c.type == "comment2") { - output.print("/*" + c.value + "*/"); - if (start.nlb) { - output.print("\n"); - output.indent(); - } else { - output.space(); - } - } - }); - } - }); - /* -----[ PARENTHESES ]----- */ function PARENS(nodetype, func) { @@ -811,14 +858,21 @@ function OutputStream(options) { self.body.print(output); output.semicolon(); }); - function print_bracketed(body, output, allow_directives) { - if (body.length > 0) output.with_block(function(){ - display_body(body, false, output, allow_directives); - }); - else output.print("{}"); + function print_bracketed(self, output, allow_directives) { + if (self.body.length > 0) { + output.with_block(function() { + display_body(self.body, false, output, allow_directives); + }); + } else { + output.print("{"); + output.with_indent(output.next_indent(), function() { + output.append_comments(self, true); + }); + output.print("}"); + } }; DEFPRINT(AST_BlockStatement, function(self, output){ - print_bracketed(self.body, output); + print_bracketed(self, output); }); DEFPRINT(AST_EmptyStatement, function(self, output){ output.semicolon(); @@ -913,7 +967,7 @@ function OutputStream(options) { }); }); output.space(); - print_bracketed(self.body, output, true); + print_bracketed(self, output, true); }); DEFPRINT(AST_Lambda, function(self, output){ self._do_print(output); @@ -1044,7 +1098,7 @@ function OutputStream(options) { DEFPRINT(AST_Try, function(self, output){ output.print("try"); output.space(); - print_bracketed(self.body, output); + print_bracketed(self, output); if (self.bcatch) { output.space(); self.bcatch.print(output); @@ -1061,12 +1115,12 @@ function OutputStream(options) { self.argname.print(output); }); output.space(); - print_bracketed(self.body, output); + print_bracketed(self, output); }); DEFPRINT(AST_Finally, function(self, output){ output.print("finally"); output.space(); - print_bracketed(self.body, output); + print_bracketed(self, output); }); /* -----[ var/const ]----- */ diff --git a/lib/parse.js b/lib/parse.js index f0098c75..41aa988b 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -317,11 +317,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { } if (!is_comment) { ret.comments_before = S.comments_before; - S.comments_before = []; - // make note of any newlines in the comments that came before - for (var i = 0, len = ret.comments_before.length; i < len; i++) { - ret.nlb = ret.nlb || ret.comments_before[i].nlb; - } + ret.comments_after = S.comments_before = []; } S.newline_before = false; return new AST_Token(ret); @@ -1280,9 +1276,16 @@ function parse($TEXT, options) { case "(": next(); var ex = expression(true); + [].push.apply(start.comments_before, ex.start.comments_before); + ex.start.comments_before = start.comments_before; + start.comments_after = ex.start.comments_after; ex.start = start; - ex.end = S.token; expect(")"); + var end = prev(); + end.comments_before = ex.end.comments_before; + [].push.apply(ex.end.comments_after, end.comments_after); + end.comments_after = ex.end.comments_after; + ex.end = end; return subscripts(ex, allow_calls); case "[": return subscripts(array_(), allow_calls); diff --git a/lib/utils.js b/lib/utils.js index 102c4789..dab7f566 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -324,7 +324,7 @@ function first_in_statement(stack) { if (p instanceof AST_Statement && p.body === node) return true; if ((p instanceof AST_Sequence && p.expressions[0] === node) || - (p instanceof AST_Call && p.expression === node && !(p instanceof AST_New) ) || + (p.TYPE == "Call" && p.expression === node ) || (p instanceof AST_Dot && p.expression === node ) || (p instanceof AST_Sub && p.expression === node ) || (p instanceof AST_Conditional && p.condition === node ) || diff --git a/test/compress/pure_funcs.js b/test/compress/pure_funcs.js index 3cc529a8..6f3bbb21 100644 --- a/test/compress/pure_funcs.js +++ b/test/compress/pure_funcs.js @@ -293,3 +293,85 @@ unary: { bar(); } } + +issue_2629_1: { + options = { + side_effects: true, + } + input: { + /*@__PURE__*/ a(); + /*@__PURE__*/ (b()); + (/*@__PURE__*/ c)(); + (/*@__PURE__*/ d()); + } + expect: {} +} + +issue_2629_2: { + options = { + side_effects: true, + } + input: { + /*@__PURE__*/ a(1)(2)(3); + /*@__PURE__*/ (b(1))(2)(3); + /*@__PURE__*/ (c(1)(2))(3); + /*@__PURE__*/ (d(1)(2)(3)); + (/*@__PURE__*/ e)(1)(2)(3); + (/*@__PURE__*/ f(1))(2)(3); + (/*@__PURE__*/ g(1)(2))(3); + (/*@__PURE__*/ h(1)(2)(3)); + } + expect: {} +} + +issue_2629_3: { + options = { + side_effects: true, + } + input: { + /*@__PURE__*/ a.x(1).y(2).z(3); + /*@__PURE__*/ (a.x)(1).y(2).z(3); + /*@__PURE__*/ (a.x(1)).y(2).z(3); + /*@__PURE__*/ (a.x(1).y)(2).z(3); + /*@__PURE__*/ (a.x(1).y(2)).z(3); + /*@__PURE__*/ (a.x(1).y(2).z)(3); + /*@__PURE__*/ (a.x(1).y(2).z(3)); + (/*@__PURE__*/ a).x(1).y(2).z(3); + (/*@__PURE__*/ a.x)(1).y(2).z(3); + (/*@__PURE__*/ a.x(1)).y(2).z(3); + (/*@__PURE__*/ a.x(1).y)(2).z(3); + (/*@__PURE__*/ a.x(1).y(2)).z(3); + (/*@__PURE__*/ a.x(1).y(2).z)(3); + (/*@__PURE__*/ a.x(1).y(2).z(3)); + } + expect: {} +} + +issue_2629_4: { + options = { + side_effects: true, + } + input: { + (/*@__PURE__*/ x(), y()); + (w(), /*@__PURE__*/ x(), y()); + } + expect: { + y(); + w(), y(); + } +} + +issue_2629_5: { + options = { + side_effects: true, + } + input: { + [ /*@__PURE__*/ x() ]; + [ /*@__PURE__*/ x(), y() ]; + [ w(), /*@__PURE__*/ x(), y() ]; + } + expect: { + y(); + w(), y(); + } +} diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js index 0e4f3dff..25233d11 100644 --- a/test/mocha/comment-filter.js +++ b/test/mocha/comment-filter.js @@ -14,7 +14,7 @@ describe("comment filters", function() { it("Should be able to filter commments with the 'some' option", function() { var ast = UglifyJS.parse("// foo\n/*@preserve*/\n// bar\n/*@license*/\n//@license with the wrong comment type\n/*@cc_on something*/"); - assert.strictEqual(ast.print_to_string({comments: "some"}), "/*@preserve*/\n/*@license*/\n/*@cc_on something*/\n"); + assert.strictEqual(ast.print_to_string({comments: "some"}), "/*@preserve*/\n/*@license*/\n/*@cc_on something*/"); }); it("Should be able to filter comments by passing a function", function() { @@ -55,12 +55,12 @@ describe("comment filters", function() { return true; }; - assert.strictEqual(ast.print_to_string({comments: f}), "#!Random comment\n//test1\n/*test2*/\n"); + assert.strictEqual(ast.print_to_string({comments: f}), "#!Random comment\n//test1\n/*test2*/"); }); it("Should never be able to filter comment5 when using 'some' as filter", function() { var ast = UglifyJS.parse("#!foo\n//foo\n/*@preserve*/\n/* please hide me */"); - assert.strictEqual(ast.print_to_string({comments: "some"}), "#!foo\n/*@preserve*/\n"); + assert.strictEqual(ast.print_to_string({comments: "some"}), "#!foo\n/*@preserve*/"); }); it("Should have no problem on multiple calls", function() { diff --git a/test/mocha/comment.js b/test/mocha/comment.js index 6b5428d4..3bad9e01 100644 --- a/test/mocha/comment.js +++ b/test/mocha/comment.js @@ -47,4 +47,176 @@ describe("Comment", function() { }, fail, tests[i]); } }); + + it("Should handle comment within return correctly", function() { + var result = uglify.minify([ + "function unequal(x, y) {", + " return (", + " // Either one", + " x < y", + " ||", + " y < x", + " );", + "}", + ].join("\n"), { + compress: false, + mangle: false, + output: { + beautify: true, + comments: "all", + }, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code, [ + "function unequal(x, y) {", + " // Either one", + " return x < y || y < x;", + "}", + ].join("\n")); + }); + + it("Should handle comment folded into return correctly", function() { + var result = uglify.minify([ + "function f() {", + " /* boo */ x();", + " return y();", + "}", + ].join("\n"), { + mangle: false, + output: { + beautify: true, + comments: "all", + }, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code, [ + "function f() {", + " /* boo */", + " return x(), y();", + "}", + ].join("\n")); + }); + + it("Should not drop comments after first OutputStream", function() { + var code = "/* boo */\nx();"; + var ast = uglify.parse(code); + var out1 = uglify.OutputStream({ + beautify: true, + comments: "all", + }); + ast.print(out1); + var out2 = uglify.OutputStream({ + beautify: true, + comments: "all", + }); + ast.print(out2); + assert.strictEqual(out1.get(), code); + assert.strictEqual(out2.get(), out1.get()); + }); + + it("Should retain trailing comments", function() { + var code = [ + "if (foo /* lost comment */ && bar /* lost comment */) {", + " // this one is kept", + " {/* lost comment */}", + " !function() {", + " // lost comment", + " }();", + " function baz() {/* lost comment */}", + " // lost comment", + "}", + "// comments right before EOF are lost as well", + ].join("\n"); + var result = uglify.minify(code, { + compress: false, + mangle: false, + output: { + beautify: true, + comments: "all", + }, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code, code); + }); + + it("Should correctly preserve new lines around comments", function() { + var tests = [ + [ + "// foo", + "// bar", + "x();", + ].join("\n"), + [ + "// foo", + "/* bar */", + "x();", + ].join("\n"), + [ + "// foo", + "/* bar */ x();", + ].join("\n"), + [ + "/* foo */", + "// bar", + "x();", + ].join("\n"), + [ + "/* foo */ // bar", + "x();", + ].join("\n"), + [ + "/* foo */", + "/* bar */", + "x();", + ].join("\n"), + [ + "/* foo */", + "/* bar */ x();", + ].join("\n"), + [ + "/* foo */ /* bar */", + "x();", + ].join("\n"), + "/* foo */ /* bar */ x();", + ].forEach(function(code) { + var result = uglify.minify(code, { + compress: false, + mangle: false, + output: { + beautify: true, + comments: "all", + }, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code, code); + }); + }); + + it("Should preserve new line before comment without beautify", function() { + var code = [ + "function f(){", + "/* foo */bar()}", + ].join("\n"); + var result = uglify.minify(code, { + compress: false, + mangle: false, + output: { + comments: "all", + }, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code, code); + }); + + it("Should preserve comments around IIFE", function() { + var result = uglify.minify("/*a*/(/*b*/function(){/*c*/}/*d*/)/*e*/();", { + compress: false, + mangle: false, + output: { + comments: "all", + }, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code, "/*a*/ /*b*/(function(){/*c*/}/*d*/ /*e*/)();"); + }); }); From c07ea17c015cb2e0fa3f48927751186ecb421481 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 24 Dec 2017 00:36:46 +0800 Subject: [PATCH 39/42] fix escape analysis on `AST_PropAccess` (#2636) --- lib/compress.js | 95 +++++++++++++++++------------------- test/compress/reduce_vars.js | 31 ++++++++++++ 2 files changed, 76 insertions(+), 50 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 85cc6ddf..5328b517 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -418,29 +418,27 @@ merge(Compressor.prototype, { } } - function mark_escaped(tw, d, scope, node, value, level) { + function mark_escaped(tw, d, scope, node, value, level, depth) { var parent = tw.parent(level); - if (value) { - if (value.is_constant()) return; - if (level > 0 && value.is_constant_expression(scope)) return; - } + if (value && value.is_constant()) return; if (parent instanceof AST_Assign && parent.operator == "=" && node === parent.right || parent instanceof AST_Call && node !== parent.expression || parent instanceof AST_Exit && node === parent.value && node.scope !== d.scope || parent instanceof AST_VarDef && node === parent.value) { - d.escaped = true; + if (depth > 1 && !(value && value.is_constant_expression(scope))) depth = 1; + if (!d.escaped || d.escaped > depth) d.escaped = depth; return; } else if (parent instanceof AST_Array || parent instanceof AST_Binary && lazy_op(parent.operator) || parent instanceof AST_Conditional && node !== parent.condition || parent instanceof AST_Sequence && node === parent.tail_node()) { - mark_escaped(tw, d, scope, parent, parent, level + 1); + mark_escaped(tw, d, scope, parent, parent, level + 1, depth); } else if (parent instanceof AST_ObjectKeyVal && node === parent.value) { var obj = tw.parent(level + 1); - mark_escaped(tw, d, scope, obj, obj, level + 2); + mark_escaped(tw, d, scope, obj, obj, level + 2, depth); } else if (parent instanceof AST_PropAccess && node === parent.expression) { value = read_property(value, parent.property); - mark_escaped(tw, d, scope, parent, value, level + 1); + mark_escaped(tw, d, scope, parent, value, level + 1, depth + 1); if (value) return; } if (level == 0) d.direct_access = true; @@ -637,7 +635,7 @@ merge(Compressor.prototype, { } } } - mark_escaped(tw, d, this.scope, this, value, 0); + mark_escaped(tw, d, this.scope, this, value, 0, 1); }); def(AST_Toplevel, function(tw, descend, compressor) { this.globals.each(function(def) { @@ -1909,7 +1907,7 @@ merge(Compressor.prototype, { // descendant of AST_Node. AST_Node.DEFMETHOD("evaluate", function(compressor){ if (!compressor.option("evaluate")) return this; - var val = this._eval(compressor); + var val = this._eval(compressor, 1); return !val || val instanceof RegExp || typeof val != "object" ? val : this; }); var unaryPrefix = makePredicate("! ~ - + void"); @@ -1928,22 +1926,17 @@ merge(Compressor.prototype, { throw new Error(string_template("Cannot evaluate a statement [{file}:{line},{col}]", this.start)); }); def(AST_Lambda, return_this); - function ev(node, compressor) { - if (!compressor) throw new Error("Compressor must be passed"); - - return node._eval(compressor); - }; def(AST_Node, return_this); def(AST_Constant, function(){ return this.getValue(); }); - def(AST_Array, function(compressor){ + def(AST_Array, function(compressor, depth) { if (compressor.option("unsafe")) { var elements = []; for (var i = 0, len = this.elements.length; i < len; i++) { var element = this.elements[i]; if (element instanceof AST_Function) continue; - var value = ev(element, compressor); + var value = element._eval(compressor, depth); if (element === value) return this; elements.push(value); } @@ -1951,7 +1944,7 @@ merge(Compressor.prototype, { } return this; }); - def(AST_Object, function(compressor){ + def(AST_Object, function(compressor, depth) { if (compressor.option("unsafe")) { var val = {}; for (var i = 0, len = this.properties.length; i < len; i++) { @@ -1960,21 +1953,21 @@ merge(Compressor.prototype, { if (key instanceof AST_Symbol) { key = key.name; } else if (key instanceof AST_Node) { - key = ev(key, compressor); + key = key._eval(compressor, depth); if (key === prop.key) return this; } if (typeof Object.prototype[key] === 'function') { return this; } if (prop.value instanceof AST_Function) continue; - val[key] = ev(prop.value, compressor); + val[key] = prop.value._eval(compressor, depth); if (val[key] === prop.value) return this; } return val; } return this; }); - def(AST_UnaryPrefix, function(compressor){ + def(AST_UnaryPrefix, function(compressor, depth) { var e = this.expression; // Function would be evaluated to an array and so typeof would // incorrectly return 'object'. Hence making is a special case. @@ -1985,7 +1978,7 @@ merge(Compressor.prototype, { && e.fixed_value() instanceof AST_Lambda)) { return typeof function(){}; } - e = ev(e, compressor); + e = e._eval(compressor, depth); if (e === this.expression) return this; switch (this.operator) { case "!": return !e; @@ -2001,10 +1994,10 @@ merge(Compressor.prototype, { } return this; }); - def(AST_Binary, function(compressor){ - var left = ev(this.left, compressor); + def(AST_Binary, function(compressor, depth) { + var left = this.left._eval(compressor, depth); if (left === this.left) return this; - var right = ev(this.right, compressor); + var right = this.right._eval(compressor, depth); if (right === this.right) return this; var result; switch (this.operator) { @@ -2038,30 +2031,32 @@ merge(Compressor.prototype, { } return result; }); - def(AST_Conditional, function(compressor){ - var condition = ev(this.condition, compressor); + def(AST_Conditional, function(compressor, depth) { + var condition = this.condition._eval(compressor, depth); if (condition === this.condition) return this; var node = condition ? this.consequent : this.alternative; - var value = ev(node, compressor); + var value = node._eval(compressor, depth); return value === node ? this : value; }); - def(AST_SymbolRef, function(compressor){ + def(AST_SymbolRef, function(compressor, depth) { var fixed = this.fixed_value(); if (!fixed) return this; - this._eval = return_this; - var value = ev(fixed, compressor); - if (value === fixed) { + var value; + if (HOP(fixed, "_eval")) { + value = fixed._eval(); + } else { + this._eval = return_this; + value = fixed._eval(compressor, depth); delete this._eval; - return this; + if (value === fixed) return this; + fixed._eval = function() { + return value; + }; } - if (!HOP(fixed, "_eval")) fixed._eval = function() { - return value; - }; - if (value && typeof value == "object" && this.definition().escaped) { - delete this._eval; - return this; + if (value && typeof value == "object") { + var escaped = this.definition().escaped; + if (escaped && depth > escaped) return this; } - this._eval = fixed._eval; return value; }); var global_objs = { @@ -2095,11 +2090,11 @@ merge(Compressor.prototype, { ], }; convert_to_predicate(static_values); - def(AST_PropAccess, function(compressor){ + def(AST_PropAccess, function(compressor, depth) { if (compressor.option("unsafe")) { var key = this.property; if (key instanceof AST_Node) { - key = ev(key, compressor); + key = key._eval(compressor, depth); if (key === this.property) return this; } var exp = this.expression; @@ -2108,7 +2103,7 @@ merge(Compressor.prototype, { if (!(static_values[exp.name] || return_false)(key)) return this; val = global_objs[exp.name]; } else { - val = ev(exp, compressor); + val = exp._eval(compressor, depth + 1); if (!val || val === exp || !HOP(val, key)) return this; } return val[key]; @@ -2186,12 +2181,12 @@ merge(Compressor.prototype, { ], }; convert_to_predicate(static_fns); - def(AST_Call, function(compressor){ + def(AST_Call, function(compressor, depth) { var exp = this.expression; if (compressor.option("unsafe") && exp instanceof AST_PropAccess) { var key = exp.property; if (key instanceof AST_Node) { - key = ev(key, compressor); + key = key._eval(compressor, depth); if (key === exp.property) return this; } var val; @@ -2200,13 +2195,13 @@ merge(Compressor.prototype, { if (!(static_fns[e.name] || return_false)(key)) return this; val = global_objs[e.name]; } else { - val = ev(e, compressor); + val = e._eval(compressor, depth + 1); if (val === e || !(val && native_fns[val.constructor.name] || return_false)(key)) return this; } var args = []; for (var i = 0, len = this.args.length; i < len; i++) { var arg = this.args[i]; - var value = ev(arg, compressor); + var value = arg._eval(compressor, depth); if (arg === value) return this; args.push(value); } @@ -3083,7 +3078,7 @@ merge(Compressor.prototype, { if (node instanceof AST_VarDef) { var sym = node.name, def, value; if (sym.scope === self - && !(def = sym.definition()).escaped + && (def = sym.definition()).escaped != 1 && !def.single_use && !def.direct_access && !top_retain(def) @@ -4698,7 +4693,7 @@ merge(Compressor.prototype, { if (d.single_use && fixed instanceof AST_Function) { if (d.scope !== self.scope && (!compressor.option("reduce_funcs") - || d.escaped + || d.escaped == 1 || fixed.inlined)) { d.single_use = false; } else if (recursive_ref(compressor, d)) { diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index bf1155b7..2b2c77da 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -3422,6 +3422,37 @@ escaped_prop_1: { } escaped_prop_2: { + options = { + collapse_vars: true, + evaluate: true, + inline: true, + passes: 2, + pure_getters: "strict", + reduce_funcs: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + var obj = { o: { a: 1 } }; + (function(o) { + o.a++; + })(obj.o); + (function(o) { + console.log(o.a); + })(obj.o); + } + expect: { + var obj = { o: { a: 1 } }; + obj.o.a++; + console.log(obj.o.a); + } + expect_stdout: "2" +} + +escaped_prop_3: { options = { reduce_funcs: true, reduce_vars: true, From 202f90ef8f2b282fbd5c063a7e5a34f79551099e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 24 Dec 2017 01:24:12 +0800 Subject: [PATCH 40/42] fix corner cases with `collapse_vars`, `inline` & `reduce_vars` (#2637) fixes #2630 --- lib/compress.js | 6 +- test/compress/functions.js | 165 +++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 5328b517..6b2c936a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1089,6 +1089,7 @@ merge(Compressor.prototype, { for (var i = stat_index; !abort && i < statements.length; i++) { statements[i].transform(multi_replacer); } + value_def.single_use = false; } } if (replaced && !remove_candidate(candidate)) statements.splice(stat_index, 1); @@ -3958,13 +3959,14 @@ merge(Compressor.prototype, { && (exp === fn ? !fn.name : compressor.option("unused") && (def = exp.definition()).references.length == 1 - && !recursive_ref(compressor, def)) + && !recursive_ref(compressor, def) + && fn.is_constant_expression(exp.scope)) && !self.has_pure_annotation(compressor) && !fn.contains_this() && (scope = can_flatten_args(fn)) && (value = flatten_body(stat))) { var expressions = flatten_args(fn, scope); - expressions.push(value); + expressions.push(value.clone(true)); return make_sequence(self, expressions).optimize(compressor); } if (compressor.option("side_effects") && all(fn.body, is_empty)) { diff --git a/test/compress/functions.js b/test/compress/functions.js index 4c22652e..07147663 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1258,3 +1258,168 @@ issue_2620_4: { } expect_stdout: "PASS" } + +issue_2630_1: { + options = { + collapse_vars: true, + inline: true, + passes: 2, + reduce_funcs: true, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + var c = 0; + (function() { + while (f()); + function f() { + var a = function() { + var b = c++, d = c = 1 + c; + }(); + } + })(); + console.log(c); + } + expect: { + var c = 0; + (function() { + while (c++, void (c = 1 + c)); + })(), + console.log(c); + } + expect_stdout: "2" +} + +issue_2630_2: { + options = { + collapse_vars: true, + inline: true, + passes: 2, + reduce_vars: true, + sequences: true, + unused: true, + } + input: { + var c = 0; + !function() { + while (f()) {} + function f() { + var not_used = function() { + c = 1 + c; + }(c = c + 1); + } + }(); + console.log(c); + } + expect: { + var c = 0; + !function() { + while (c += 1, void (c = 1 + c)); + }(), console.log(c); + } + expect_stdout: "2" +} + +issue_2630_3: { + options = { + inline: true, + reduce_vars: true, + unused: true, + } + input: { + var x = 2, a = 1; + (function() { + function f1(a) { + f2(); + --x >= 0 && f1({}); + } + f1(a++); + function f2() { + a++; + } + })(); + console.log(a); + } + expect: { + var x = 2, a = 1; + (function() { + function f1(a) { + f2(); + --x >= 0 && f1({}); + } + f1(a++); + function f2() { + a++; + } + })(); + console.log(a); + } + expect_stdout: "5" +} + +issue_2630_4: { + options = { + collapse_vars: true, + inline: true, + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + var x = 3, a = 1, b = 2; + (function() { + (function f1() { + while (--x >= 0 && f2()); + }()); + function f2() { + a++ + (b += a); + } + })(); + console.log(a); + } + expect: { + var x = 3, a = 1, b = 2; + (function() { + (function() { + while (--x >= 0 && void (a++, b += a)); + })(); + })(); + console.log(a); + } + expect_stdout: "2" +} + +issue_2630_5: { + options = { + collapse_vars: true, + inline: true, + reduce_vars: true, + unused: true, + } + input: { + var c = 1; + !function() { + do { + c *= 10; + } while (f()); + function f() { + return function() { + return (c = 2 + c) < 100; + }(c = c + 3); + } + }(); + console.log(c); + } + expect: { + var c = 1; + !function() { + do { + c *= 10; + } while (c += 3, (c = 2 + c) < 100); + }(); + console.log(c); + } + expect_stdout: "155" +} From efffb817357898f3a05d24fdddbf3280e33bf880 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 24 Dec 2017 12:38:45 +0800 Subject: [PATCH 41/42] fix comments output & improve `/*@__PURE__*/` - fix whitespace around comments - fix comment parsing around parentheses - consider parentheses when parsing `/*@__PURE__*/` - remove all `/*@__PURE__*/` on output fixes #2638 --- lib/compress.js | 21 ++--------- lib/output.js | 34 ++++++++++++++---- lib/parse.js | 33 ++++++++++++++--- test/compress/pure_funcs.js | 71 ++++++++++++++++++++++++++++--------- test/mocha/minify.js | 4 +-- 5 files changed, 116 insertions(+), 47 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 6b2c936a..315a18c7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2292,29 +2292,13 @@ merge(Compressor.prototype, { }); }); - AST_Call.DEFMETHOD("has_pure_annotation", function(compressor) { - if (!compressor.option("side_effects")) return false; - if (this.pure !== undefined) return this.pure; - var pure = false; - var comments, pure_comment; - if (this.start - && (comments = this.start.comments_before) - && comments.length - && (pure_comment = find_if(function (comment) { - return /[@#]__PURE__/.test(comment.value); - }, comments))) { - pure = pure_comment; - } - return this.pure = pure; - }); - var global_pure_fns = makePredicate("Boolean decodeURI decodeURIComponent Date encodeURI encodeURIComponent Error escape EvalError isFinite isNaN Number Object parseFloat parseInt RangeError ReferenceError String SyntaxError TypeError unescape URIError"); AST_Call.DEFMETHOD("is_expr_pure", function(compressor) { if (compressor.option("unsafe")) { var expr = this.expression; if (is_undeclared_ref(expr) && global_pure_fns(expr.name)) return true; } - return this.has_pure_annotation(compressor) || !compressor.pure_funcs(this); + return this.pure || !compressor.pure_funcs(this); }); // determine if expression has side effects @@ -3164,7 +3148,6 @@ merge(Compressor.prototype, { } if (this.pure) { compressor.warn("Dropping __PURE__ call [{file}:{line},{col}]", this.start); - this.pure.value = this.pure.value.replace(/[@#]__PURE__/g, ' '); } var args = trim(this.args, compressor, first_in_statement); return args && make_sequence(this, args); @@ -3961,7 +3944,7 @@ merge(Compressor.prototype, { && (def = exp.definition()).references.length == 1 && !recursive_ref(compressor, def) && fn.is_constant_expression(exp.scope)) - && !self.has_pure_annotation(compressor) + && !self.pure && !fn.contains_this() && (scope = can_flatten_args(fn)) && (value = flatten_body(stat))) { diff --git a/lib/output.js b/lib/output.js index b0cecf06..fc592d60 100644 --- a/lib/output.js +++ b/lib/output.js @@ -201,6 +201,8 @@ function OutputStream(options) { var might_need_semicolon = false; var might_add_newline = 0; var need_newline_indented = false; + var need_space = false; + var newline_insert = -1; var last = ""; var mapping_token, mapping_name, mappings = options.source_map && []; @@ -266,6 +268,13 @@ function OutputStream(options) { indent(); } } + if (need_space && ch) { + need_space = false; + if (!/[\s;})]/.test(ch)) { + space(); + } + } + newline_insert = -1; var prev = last.charAt(last.length - 1); if (might_need_semicolon) { might_need_semicolon = false; @@ -364,7 +373,13 @@ function OutputStream(options) { } : function(col, cont) { return cont() }; var newline = options.beautify ? function() { - print("\n"); + if (newline_insert < 0) return print("\n"); + if (OUTPUT[newline_insert] != "\n") { + OUTPUT = OUTPUT.slice(0, newline_insert) + "\n" + OUTPUT.slice(newline_insert); + current_pos++; + current_line++; + } + newline_insert++; } : options.max_line_len ? function() { ensure_line_len(); might_add_newline = OUTPUT.length; @@ -495,11 +510,11 @@ function OutputStream(options) { } } if (/comment[134]/.test(c.type)) { - print("//" + c.value + "\n"); + print("//" + c.value.replace(/[@#]__PURE__/g, ' ') + "\n"); indent(); last_nlb = true; } else if (c.type == "comment2") { - print("/*" + c.value + "*/"); + print("/*" + c.value.replace(/[@#]__PURE__/g, ' ') + "*/"); last_nlb = false; } }); @@ -521,21 +536,28 @@ function OutputStream(options) { var comments = token[tail ? "comments_before" : "comments_after"]; if (comments && comments._dumped !== self) { comments._dumped = self; + var insert = OUTPUT.length; comments.filter(comment_filter, node).forEach(function(c, i) { - if (need_newline_indented || c.nlb) { + need_space = false; + if (need_newline_indented) { print("\n"); indent(); need_newline_indented = false; + } else if (c.nlb && (i > 0 || !/(^|\n) *$/.test(OUTPUT))) { + print("\n"); + indent(); } else if (i > 0 || !tail) { space(); } if (/comment[134]/.test(c.type)) { - print("//" + c.value); + print("//" + c.value.replace(/[@#]__PURE__/g, ' ')); need_newline_indented = true; } else if (c.type == "comment2") { - print("/*" + c.value + "*/"); + print("/*" + c.value.replace(/[@#]__PURE__/g, ' ') + "*/"); + need_space = true; } }); + if (OUTPUT.length > insert) newline_insert = insert; } } diff --git a/lib/parse.js b/lib/parse.js index 41aa988b..c042a60b 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1276,8 +1276,17 @@ function parse($TEXT, options) { case "(": next(); var ex = expression(true); - [].push.apply(start.comments_before, ex.start.comments_before); - ex.start.comments_before = start.comments_before; + var len = start.comments_before.length; + [].unshift.apply(ex.start.comments_before, start.comments_before); + start.comments_before = ex.start.comments_before; + start.comments_before_length = len; + if (len == 0 && start.comments_before.length > 0) { + var comment = start.comments_before[0]; + if (!comment.nlb) { + comment.nlb = start.nlb; + start.nlb = false; + } + } start.comments_after = ex.start.comments_after; ex.start = start; expect(")"); @@ -1286,6 +1295,7 @@ function parse($TEXT, options) { [].push.apply(ex.end.comments_after, end.comments_after); end.comments_after = ex.end.comments_after; ex.end = end; + if (ex instanceof AST_Call) mark_pure(ex); return subscripts(ex, allow_calls); case "[": return subscripts(array_(), allow_calls); @@ -1433,6 +1443,19 @@ function parse($TEXT, options) { return sym; }; + function mark_pure(call) { + var start = call.start; + var comments = start.comments_before; + var i = HOP(start, "comments_before_length") ? start.comments_before_length : comments.length; + while (--i >= 0) { + var comment = comments[i]; + if (/[@#]__PURE__/.test(comment.value)) { + call.pure = comment; + break; + } + } + } + var subscripts = function(expr, allow_calls) { var start = expr.start; if (is("punc", ".")) { @@ -1457,12 +1480,14 @@ function parse($TEXT, options) { } if (allow_calls && is("punc", "(")) { next(); - return subscripts(new AST_Call({ + var call = new AST_Call({ start : start, expression : expr, args : expr_list(")"), end : prev() - }), true); + }); + mark_pure(call); + return subscripts(call, true); } return expr; }; diff --git a/test/compress/pure_funcs.js b/test/compress/pure_funcs.js index 6f3bbb21..d15bcca3 100644 --- a/test/compress/pure_funcs.js +++ b/test/compress/pure_funcs.js @@ -298,19 +298,27 @@ issue_2629_1: { options = { side_effects: true, } + beautify = { + comments: "all", + } input: { /*@__PURE__*/ a(); /*@__PURE__*/ (b()); (/*@__PURE__*/ c)(); (/*@__PURE__*/ d()); } - expect: {} + expect_exact: [ + "/* */c();", + ] } issue_2629_2: { options = { side_effects: true, } + beautify = { + comments: "all", + } input: { /*@__PURE__*/ a(1)(2)(3); /*@__PURE__*/ (b(1))(2)(3); @@ -321,30 +329,44 @@ issue_2629_2: { (/*@__PURE__*/ g(1)(2))(3); (/*@__PURE__*/ h(1)(2)(3)); } - expect: {} + expect_exact: [ + "/* */e(1)(2)(3);", + "/* */f(1)(2)(3);", + "/* */g(1)(2)(3);", + ] } issue_2629_3: { options = { side_effects: true, } + beautify = { + comments: "all", + } input: { /*@__PURE__*/ a.x(1).y(2).z(3); - /*@__PURE__*/ (a.x)(1).y(2).z(3); - /*@__PURE__*/ (a.x(1)).y(2).z(3); - /*@__PURE__*/ (a.x(1).y)(2).z(3); - /*@__PURE__*/ (a.x(1).y(2)).z(3); - /*@__PURE__*/ (a.x(1).y(2).z)(3); - /*@__PURE__*/ (a.x(1).y(2).z(3)); - (/*@__PURE__*/ a).x(1).y(2).z(3); - (/*@__PURE__*/ a.x)(1).y(2).z(3); - (/*@__PURE__*/ a.x(1)).y(2).z(3); - (/*@__PURE__*/ a.x(1).y)(2).z(3); - (/*@__PURE__*/ a.x(1).y(2)).z(3); - (/*@__PURE__*/ a.x(1).y(2).z)(3); - (/*@__PURE__*/ a.x(1).y(2).z(3)); + /*@__PURE__*/ (b.x)(1).y(2).z(3); + /*@__PURE__*/ (c.x(1)).y(2).z(3); + /*@__PURE__*/ (d.x(1).y)(2).z(3); + /*@__PURE__*/ (e.x(1).y(2)).z(3); + /*@__PURE__*/ (f.x(1).y(2).z)(3); + /*@__PURE__*/ (g.x(1).y(2).z(3)); + (/*@__PURE__*/ h).x(1).y(2).z(3); + (/*@__PURE__*/ i.x)(1).y(2).z(3); + (/*@__PURE__*/ j.x(1)).y(2).z(3); + (/*@__PURE__*/ k.x(1).y)(2).z(3); + (/*@__PURE__*/ l.x(1).y(2)).z(3); + (/*@__PURE__*/ m.x(1).y(2).z)(3); + (/*@__PURE__*/ n.x(1).y(2).z(3)); } - expect: {} + expect_exact: [ + "/* */h.x(1).y(2).z(3);", + "/* */i.x(1).y(2).z(3);", + "/* */j.x(1).y(2).z(3);", + "/* */k.x(1).y(2).z(3);", + "/* */l.x(1).y(2).z(3);", + "/* */m.x(1).y(2).z(3);", + ] } issue_2629_4: { @@ -375,3 +397,20 @@ issue_2629_5: { w(), y(); } } + +issue_2638: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/(g() || h())(x(), y()); + (/*@__PURE__*/ (a() || b()))(c(), d()); + } + expect_exact: [ + "/* */x(),y();", + "/* */(a()||b())(c(),d());", + ] +} diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 5d9512f3..5fa9254b 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -247,7 +247,7 @@ describe("minify", function() { var code = result.code; assert.strictEqual(code, "// comment1 comment2\nbar();"); }); - it("should not drop #__PURE__ hint if function is retained", function() { + it("should drop #__PURE__ hint if function is retained", function() { var result = Uglify.minify("var a = /*#__PURE__*/(function(){ foo(); })();", { output: { comments: "all", @@ -255,7 +255,7 @@ describe("minify", function() { } }); var code = result.code; - assert.strictEqual(code, "var a=/*#__PURE__*/function(){foo()}();"); + assert.strictEqual(code, "var a=/* */function(){foo()}();"); }) }); From f1556cb9451e9532896f9e553087c9ce83801170 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 24 Dec 2017 17:34:56 +0800 Subject: [PATCH 42/42] v3.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3f21d63..f42ac272 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.2.2", + "version": "3.3.0", "engines": { "node": ">=0.8.0" },