From 333792352e2d0108622c318e9e3a4ee96ceb3346 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 31 Dec 2017 16:15:00 +0800 Subject: [PATCH 01/29] reduce hoisting declarations (#2687) --- lib/compress.js | 33 +++++++++++++---------- test/compress/reduce_vars.js | 51 ++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index b94e5847..604371fb 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -325,9 +325,14 @@ merge(Compressor.prototype, { def.single_use = undefined; } - function reset_variables(compressor, node) { + function reset_variables(tw, compressor, node) { node.variables.each(function(def) { reset_def(compressor, def); + if (def.fixed === undefined && def.orig[0].TYPE == "SymbolVar") { + def.fixed = null; + def.safe_ids = tw.safe_ids; + mark(tw, def, true); + } }); } @@ -356,6 +361,11 @@ merge(Compressor.prototype, { } function safe_to_assign(tw, def, value) { + if (def.fixed === null && def.safe_ids) { + def.safe_ids[def.id] = false; + delete def.safe_ids; + return true; + } if (!HOP(tw.safe_ids, def.id)) return false; if (!safe_to_read(tw, def)) return false; if (def.fixed === false) return false; @@ -462,10 +472,7 @@ merge(Compressor.prototype, { 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; - })) { + if (safe_to_assign(tw, d, node.right)) { d.references.push(node.left); d.fixed = function() { return node.right; @@ -495,7 +502,6 @@ merge(Compressor.prototype, { 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)) { @@ -508,6 +514,7 @@ merge(Compressor.prototype, { } var save_ids = tw.safe_ids; tw.safe_ids = Object.create(null); + reset_variables(tw, compressor, this); descend(); tw.safe_ids = save_ids; return true; @@ -555,9 +562,9 @@ merge(Compressor.prototype, { }); def(AST_Function, function(tw, descend, compressor) { var node = this; - reset_variables(compressor, node); node.inlined = false; push(tw); + reset_variables(tw, compressor, node); var iife; if (!node.name && (iife = tw.parent()) instanceof AST_Call @@ -644,7 +651,7 @@ merge(Compressor.prototype, { this.globals.each(function(def) { reset_def(compressor, def); }); - reset_variables(compressor, this); + reset_variables(tw, compressor, this); }); def(AST_Try, function(tw) { push(tw); @@ -669,8 +676,6 @@ merge(Compressor.prototype, { tw.loop_ids[d.id] = tw.in_loop; mark(tw, d, false); descend(); - } else { - d.fixed = null; } mark(tw, d, true); return true; @@ -4008,8 +4013,8 @@ merge(Compressor.prototype, { 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)); + if (value) { + expressions.unshift(value); } } else { var def = name.definition(); @@ -4024,10 +4029,10 @@ merge(Compressor.prototype, { })); var sym = make_node(AST_SymbolRef, name, name); def.references.push(sym); - expressions.unshift(make_node(AST_Assign, self, { + if (value) expressions.unshift(make_node(AST_Assign, self, { operator: "=", left: sym, - right: value || make_node(AST_Undefined, self) + right: value })); } } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 6f302ecd..058e9dde 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2312,8 +2312,7 @@ delay_def: { } expect: { function f() { - return a; - var a; + return; } function g() { return a; @@ -2324,6 +2323,28 @@ delay_def: { expect_stdout: true } +delay_def_lhs: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + console.log(function() { + long_name++; + return long_name; + var long_name; + }()); + } + expect: { + console.log(function() { + long_name++; + return long_name; + var long_name; + }()); + } + expect_stdout: "NaN" +} + booleans: { options = { booleans: true, @@ -4952,3 +4973,29 @@ issue_2598: { } expect_stdout: "true" } + +var_if: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + function f() { + if (x()) { + var a; + if (!g) a = true; + if (a) g(); + } + } + } + expect: { + function f() { + if (x()) { + var a; + if (!g) a = true; + if (a) g(); + } + } + } +} From da82fa59a723b22ad5d8dec42cd2ca0d070f2359 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 31 Dec 2017 20:59:58 +0800 Subject: [PATCH 02/29] fix `inline` on duplicate argument names (#2698) --- lib/compress.js | 2 +- test/compress/functions.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 604371fb..56176e55 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4012,7 +4012,7 @@ merge(Compressor.prototype, { for (var len = fn.argnames.length, i = len; --i >= 0;) { var name = fn.argnames[i]; var value = self.args[i]; - if (name.__unused) { + if (name.__unused || scope.var_names()[name.name]) { if (value) { expressions.unshift(value); } diff --git a/test/compress/functions.js b/test/compress/functions.js index 888c6e3c..e6c4301c 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1646,3 +1646,28 @@ issue_2663_3: { "reset", ] } + +duplicate_argnames: { + options = { + inline: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a = "PASS"; + function f(b, b, b) { + b && (a = "FAIL"); + } + f(0, console); + console.log(a); + } + expect: { + var a = "PASS"; + console, b && (a = "FAIL"); + var b; + console.log(a); + } + expect_stdout: "PASS" +} From 673b0716379e261008b6cbf187e6b212104fb69e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 1 Jan 2018 00:09:26 +0800 Subject: [PATCH 03/29] enhance `join_vars` & `sequences` (#2697) - nudge declarations without assignments - within `AST_BlockStatement` - across `AST_If` --- lib/compress.js | 92 ++++++++++++++++++++++++++++------- test/compress/conditionals.js | 21 ++++++++ test/compress/functions.js | 2 +- test/compress/sequences.js | 63 ++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 18 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 56176e55..ee80901e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -884,6 +884,7 @@ merge(Compressor.prototype, { } if (compressor.sequences_limit > 0) { sequencesize(statements, compressor); + sequencesize_2(statements, compressor); } if (compressor.option("join_vars")) { join_consecutive_vars(statements, compressor); @@ -1537,6 +1538,12 @@ merge(Compressor.prototype, { }); } + function declarations_only(node) { + return all(node.definitions, function(var_def) { + return !var_def.value; + }); + } + function sequencesize(statements, compressor) { if (statements.length < 2) return; var seq = [], n = 0; @@ -1553,6 +1560,9 @@ merge(Compressor.prototype, { var body = stat.body; if (seq.length > 0) body = body.drop_side_effect_free(compressor); if (body) merge_sequence(seq, body); + } else if (stat instanceof AST_Definitions && declarations_only(stat) + || stat instanceof AST_Defun) { + statements[n++] = stat; } else { push_seq(); statements[n++] = stat; @@ -1560,13 +1570,31 @@ merge(Compressor.prototype, { } push_seq(); statements.length = n; - sequencesize_2(statements, compressor); - CHANGED = statements.length != len; + if (n != len) CHANGED = true; + } + + function to_simple_statement(block, decls) { + if (!(block instanceof AST_BlockStatement)) return block; + var defs = []; + var stat = null; + for (var i = 0, len = block.body.length; i < len; i++) { + var line = block.body[i]; + if (line instanceof AST_Definitions && declarations_only(line)) { + defs.push(line); + } else if (stat) { + return false; + } else { + stat = line; + } + } + [].push.apply(decls, defs); + return stat; } function sequencesize_2(statements, compressor) { function cons_seq(right) { n--; + CHANGED = true; var left = prev.body; return make_sequence(left, [ left, right ]).transform(compressor); }; @@ -1588,6 +1616,7 @@ merge(Compressor.prototype, { else { stat.init = prev.body; n--; + CHANGED = true; } } } @@ -1607,6 +1636,22 @@ merge(Compressor.prototype, { stat.expression = cons_seq(stat.expression); } } + if (compressor.option("conditionals") && stat instanceof AST_If) { + var decls = []; + var body = to_simple_statement(stat.body, decls); + var alt = to_simple_statement(stat.alternative, decls); + if (body !== false && alt !== false && decls.length > 0) { + decls.push(make_node(AST_If, stat, { + condition: stat.condition, + body: body || make_node(AST_EmptyStatement, stat.body), + alternative: alt + })); + stat = make_node(AST_BlockStatement, stat, { + body: decls + }); + CHANGED = true; + } + } statements[n++] = stat; prev = stat instanceof AST_SimpleStatement ? stat : null; } @@ -1614,30 +1659,43 @@ merge(Compressor.prototype, { } function join_consecutive_vars(statements, compressor) { + var defs; for (var i = 0, j = -1, len = statements.length; i < len; i++) { var stat = statements[i]; var prev = statements[j]; - if (stat instanceof AST_Definitions && prev && prev.TYPE == stat.TYPE) { - prev.definitions = prev.definitions.concat(stat.definitions); - CHANGED = true; - } - else if (stat instanceof AST_For - && prev instanceof AST_Var - && (!stat.init || stat.init.TYPE == prev.TYPE)) { - CHANGED = true; - if (stat.init) { - stat.init.definitions = prev.definitions.concat(stat.init.definitions); + if (stat instanceof AST_Definitions) { + if (prev && prev.TYPE == stat.TYPE) { + prev.definitions = prev.definitions.concat(stat.definitions); + CHANGED = true; + } else if (defs && defs.TYPE == stat.TYPE && declarations_only(stat)) { + defs.definitions = defs.definitions.concat(stat.definitions); + CHANGED = true; } else { - stat.init = prev; + statements[++j] = stat; + defs = stat; } - statements[j] = stat; - } - else { + } else if (stat instanceof AST_For) { + if (prev instanceof AST_Var && (!stat.init || stat.init.TYPE == prev.TYPE)) { + if (stat.init) { + prev.definitions = prev.definitions.concat(stat.init.definitions); + } + stat.init = prev; + statements[j] = stat; + CHANGED = true; + } else if (defs && stat.init && defs.TYPE == stat.init.TYPE && declarations_only(stat.init)) { + defs.definitions = defs.definitions.concat(stat.init.definitions); + stat.init = null; + statements[++j] = stat; + CHANGED = true; + } else { + statements[++j] = stat; + } + } else { statements[++j] = stat; } } statements.length = j + 1; - }; + } } function extract_declarations_from_unreachable_code(compressor, stat, target) { diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 4d61d39f..7838fdbb 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -1203,3 +1203,24 @@ issue_2560: { "2", ] } + +hoist_decl: { + options = { + conditionals: true, + join_vars: true, + sequences: true, + } + input: { + if (x()) { + var a; + y(); + } else { + z(); + var b; + } + } + expect: { + var a, b; + x() ? y() : z(); + } +} diff --git a/test/compress/functions.js b/test/compress/functions.js index e6c4301c..0e37b78a 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -800,12 +800,12 @@ issue_2601_1: { expect: { var a = "FAIL"; (function() { + var b; b = "foo", function(b) { b && b(); }(), b && (a = "PASS"); - var b; })(), console.log(a); } diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 81b06881..3d12fb0b 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -796,3 +796,66 @@ cascade_assignment_in_return: { } } } + +hoist_defun: { + options = { + join_vars: true, + sequences: true, + } + input: { + x(); + function f() {} + y(); + } + expect: { + function f() {} + x(), y(); + } +} + +hoist_decl: { + options = { + join_vars: true, + sequences: true, + } + input: { + var a; + w(); + var b = x(); + y(); + for (var c; 0;) z(); + var d; + } + expect: { + var a; + w(); + var b = x(), c, d; + for (y(); 0;) z(); + } +} + +for_init_var: { + options = { + join_vars: true, + unused: false, + } + input: { + var a = "PASS"; + (function() { + var b = 42; + for (var c = 5; c > 0;) c--; + a = "FAIL"; + var a; + })(); + console.log(a); + } + expect: { + var a = "PASS"; + (function() { + for (var b = 42, c = 5, a; c > 0;) c--; + a = "FAIL"; + })(); + console.log(a); + } + expect_stdout: "PASS" +} From 2f3bddbacaaafb73ed046090a74ce6558c54a218 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 2 Jan 2018 01:24:23 +0800 Subject: [PATCH 04/29] scan within IIFEs of assigned values (#2702) fixes #2701 --- lib/compress.js | 5 ++--- test/compress/dead-code.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index ee80901e..73cc9d7b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4913,9 +4913,8 @@ merge(Compressor.prototype, { if (reachable) return true; if (node instanceof AST_Scope && node !== self) { var parent = scan_scope.parent(); - if (!(parent instanceof AST_Call && parent.expression === node)) { - node.walk(find_ref); - } + if (parent instanceof AST_Call && parent.expression === node) return; + node.walk(find_ref); return true; } }); diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 68ee4b12..32bb88e6 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -881,3 +881,31 @@ issue_2692: { } expect_stdout: "function" } + +issue_2701: { + options = { + dead_code: true, + inline: false, + } + input: { + function f(a) { + return a = function() { + return function() { + return a; + }; + }(); + } + console.log(typeof f()()); + } + expect: { + function f(a) { + return a = function() { + return function() { + return a; + }; + }(); + } + console.log(typeof f()()); + } + expect_stdout: "function" +} From d838b4b52e4b7bea5e2f473469c07f6a3cce397d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 2 Jan 2018 01:24:53 +0800 Subject: [PATCH 05/29] reset argument value within loop after `inline` (#2699) --- lib/compress.js | 11 ++++++----- test/compress/functions.js | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 73cc9d7b..203c1443 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3991,7 +3991,7 @@ merge(Compressor.prototype, { } } if (is_func) { - var def, value, scope, level = -1; + var def, value, scope, in_loop, level = -1; if (compressor.option("inline") && !fn.uses_arguments && !fn.uses_eval @@ -4038,13 +4038,13 @@ merge(Compressor.prototype, { return self; function can_flatten_args(fn) { - var catches = Object.create(null), defs; + var catches = Object.create(null); do { scope = compressor.parent(++level); if (scope instanceof AST_Catch) { catches[scope.argname.name] = true; } else if (scope instanceof AST_IterationStatement) { - defs = []; + in_loop = []; } else if (scope instanceof AST_SymbolRef) { if (scope.fixed_value() instanceof AST_Scope) return false; } @@ -4059,9 +4059,9 @@ merge(Compressor.prototype, { || scope.var_names()[arg.name]) { return false; } - if (defs) defs.push(arg.definition()); + if (in_loop) in_loop.push(arg.definition()); } - return !defs || defs.length == 0 || !is_reachable(stat, defs); + return !in_loop || in_loop.length == 0 || !is_reachable(stat, in_loop); } function flatten_args(fn) { @@ -4087,6 +4087,7 @@ merge(Compressor.prototype, { })); var sym = make_node(AST_SymbolRef, name, name); def.references.push(sym); + if (!value && in_loop) value = make_node(AST_Undefined, self); if (value) expressions.unshift(make_node(AST_Assign, self, { operator: "=", left: sym, diff --git a/test/compress/functions.js b/test/compress/functions.js index 0e37b78a..7acb7fde 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1671,3 +1671,26 @@ duplicate_argnames: { } expect_stdout: "PASS" } + +loop_init_arg: { + options = { + inline: true, + side_effects: true, + toplevel: true, + } + input: { + var a = "PASS"; + for (var k in "12") (function (b) { + (b >>= 1) && (a = "FAIL"), b = 2; + })(); + console.log(a); + } + expect: { + var a = "PASS"; + for (var k in "12") + b = void 0, (b >>= 1) && (a = "FAIL"), b = 2; + var b; + console.log(a); + } + expect_stdout: "PASS" +} From cc931b3ad85ff980e397d598f49fb649d08bc68c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 2 Jan 2018 15:09:51 +0800 Subject: [PATCH 06/29] enhance `if_return` (#2703) --- lib/compress.js | 54 ++++++++++++++++++++++++++++---------- test/compress/if_return.js | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 203c1443..5da07f71 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1343,12 +1343,13 @@ merge(Compressor.prototype, { var in_lambda = self instanceof AST_Lambda; for (var i = statements.length; --i >= 0;) { var stat = statements[i]; - var next = statements[i + 1]; + var j = next_index(i); + var next = statements[j]; if (in_lambda && !next && stat instanceof AST_Return) { if (!stat.value) { CHANGED = true; - statements.length--; + statements.splice(i, 1); continue; } if (stat.value instanceof AST_UnaryPrefix && stat.value.operator == "void") { @@ -1418,7 +1419,8 @@ merge(Compressor.prototype, { CHANGED = true; stat = stat.clone(); stat.alternative = next; - statements.splice(i, 2, stat.transform(compressor)); + statements.splice(i, 1, stat.transform(compressor)); + statements.splice(j, 1); continue; } //--- @@ -1430,7 +1432,8 @@ merge(Compressor.prototype, { stat.alternative = next || make_node(AST_Return, stat, { value: null }); - statements.splice(i, next ? 2 : 1, stat.transform(compressor)); + statements.splice(i, 1, stat.transform(compressor)); + if (next) statements.splice(j, 1); continue; } //--- @@ -1439,10 +1442,10 @@ merge(Compressor.prototype, { // if sequences is not enabled, this can lead to an endless loop (issue #866). // however, with sequences on this helps producing slightly better output for // the example code. - var prev = statements[i - 1]; + var prev = statements[prev_index(i)]; if (compressor.option("sequences") && in_lambda && !stat.alternative && prev instanceof AST_If && prev.body instanceof AST_Return - && i + 2 == statements.length && next instanceof AST_SimpleStatement) { + && next_index(j) == statements.length && next instanceof AST_SimpleStatement) { CHANGED = true; stat = stat.clone(); stat.alternative = make_node(AST_BlockStatement, next, { @@ -1453,7 +1456,8 @@ merge(Compressor.prototype, { }) ] }); - statements.splice(i, 2, stat.transform(compressor)); + statements.splice(i, 1, stat.transform(compressor)); + statements.splice(j, 1); continue; } } @@ -1503,6 +1507,26 @@ merge(Compressor.prototype, { } return body; } + + function next_index(i) { + for (var j = i + 1, len = statements.length; j < len; j++) { + var stat = statements[j]; + if (!(stat instanceof AST_Definitions && declarations_only(stat))) { + break; + } + } + return j; + } + + function prev_index(i) { + for (var j = i; --j >= 0;) { + var stat = statements[j]; + if (!(stat instanceof AST_Definitions && declarations_only(stat))) { + break; + } + } + return j; + } } function eliminate_dead_code(statements, compressor) { @@ -1575,19 +1599,17 @@ merge(Compressor.prototype, { function to_simple_statement(block, decls) { if (!(block instanceof AST_BlockStatement)) return block; - var defs = []; var stat = null; for (var i = 0, len = block.body.length; i < len; i++) { var line = block.body[i]; if (line instanceof AST_Definitions && declarations_only(line)) { - defs.push(line); + decls.push(line); } else if (stat) { return false; } else { stat = line; } } - [].push.apply(decls, defs); return stat; } @@ -1599,7 +1621,7 @@ merge(Compressor.prototype, { return make_sequence(left, [ left, right ]).transform(compressor); }; var n = 0, prev; - for (var i = 0, len = statements.length; i < len; i++) { + for (var i = 0; i < statements.length; i++) { var stat = statements[i]; if (prev) { if (stat instanceof AST_For && !(stat.init instanceof AST_Definitions)) { @@ -1641,15 +1663,19 @@ merge(Compressor.prototype, { var body = to_simple_statement(stat.body, decls); var alt = to_simple_statement(stat.alternative, decls); if (body !== false && alt !== false && decls.length > 0) { + var len = decls.length; decls.push(make_node(AST_If, stat, { condition: stat.condition, body: body || make_node(AST_EmptyStatement, stat.body), alternative: alt })); - stat = make_node(AST_BlockStatement, stat, { - body: decls - }); + decls.unshift(n, 1); + [].splice.apply(statements, decls); + i += len; + n += len + 1; + prev = null; CHANGED = true; + continue; } } statements[n++] = stat; diff --git a/test/compress/if_return.js b/test/compress/if_return.js index 72b69e70..a0dfdc9a 100644 --- a/test/compress/if_return.js +++ b/test/compress/if_return.js @@ -326,3 +326,49 @@ issue_512: { } } } + +if_var_return: { + options = { + conditionals: true, + if_return: true, + join_vars: true, + sequences: true, + } + input: { + function f() { + var a; + return; + var b; + } + function g() { + var a; + if (u()) { + var b; + return v(); + var c; + } + var d; + if (w()) { + var e; + return x(); + var f; + } else { + var g; + y(); + var h; + } + var i; + z(); + var j; + } + } + expect: { + function f() { + var a, b; + } + function g() { + var a, b, c, d, e, f, g, h, i, j; + return u() ? v() : w() ? x() : (y(), z(), void 0); + } + } +} From 6dead95eb3e2566f157b9d776803ea8b7168356d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 2 Jan 2018 18:42:15 +0800 Subject: [PATCH 07/29] enhance `collapse_vars` (#2704) --- lib/compress.js | 19 ++++++++++++++----- test/compress/functions.js | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 5da07f71..32212688 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1159,10 +1159,10 @@ merge(Compressor.prototype, { 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()); - } + extract_candidates(expr.right); + } else if (expr instanceof AST_Binary) { + extract_candidates(expr.left); + extract_candidates(expr.right); } else if (expr instanceof AST_Call) { extract_candidates(expr.expression); expr.args.forEach(extract_candidates); @@ -1187,14 +1187,22 @@ merge(Compressor.prototype, { } else if (expr instanceof AST_Switch) { extract_candidates(expr.expression); expr.body.forEach(extract_candidates); + } else if (expr instanceof AST_Unary) { + if (expr.operator == "++" || expr.operator == "--") { + candidates.push(hit_stack.slice()); + } } else if (expr instanceof AST_VarDef) { - if (expr.value) candidates.push(hit_stack.slice()); + if (expr.value) { + candidates.push(hit_stack.slice()); + extract_candidates(expr.value); + } } hit_stack.pop(); } function find_stop(node, level) { var parent = scanner.parent(level); + if (parent instanceof AST_Binary) return node; if (parent instanceof AST_Call) return node; if (parent instanceof AST_Case) return node; if (parent instanceof AST_Conditional) return node; @@ -1202,6 +1210,7 @@ merge(Compressor.prototype, { 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; + if (parent instanceof AST_VarDef) return node; return null; } diff --git a/test/compress/functions.js b/test/compress/functions.js index 7acb7fde..dd98ba60 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1285,7 +1285,7 @@ issue_2630_1: { expect: { var c = 0; (function() { - while (c++, void (c = 1 + c)); + while (void (c = 1 + ++c)); })(), console.log(c); } @@ -1316,7 +1316,7 @@ issue_2630_2: { expect: { var c = 0; !function() { - while (c += 1, void (c = 1 + c)); + while (void (c = 1 + (c += 1))); }(), console.log(c); } expect_stdout: "2" From 7d3cddf9d624d169c6667a52e8d6f313d1b30159 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 3 Jan 2018 01:54:44 +0800 Subject: [PATCH 08/29] inline functions with `AST_Var` (#2688) --- README.md | 8 +- lib/compress.js | 174 +++++++++++++++++++--------- test/compress/functions.js | 231 +++++++++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 43793dcd..d7c47d72 100644 --- a/README.md +++ b/README.md @@ -640,7 +640,13 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `if_return` (default: `true`) -- optimizations for if/return and if/continue -- `inline` (default: `true`) -- embed simple functions +- `inline` (default: `true`) -- inline calls to function with simple/`return` statement: + - `false` -- same as `0` + - `0` -- disabled inlining + - `1` -- inline simple functions + - `2` -- inline functions with arguments + - `3` -- inline functions with arguments and variables + - `true` -- same as `3` - `join_vars` (default: `true`) -- join consecutive `var` statements diff --git a/lib/compress.js b/lib/compress.js index 32212688..06873df8 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -99,6 +99,7 @@ function Compressor(options, false_by_default) { }); } } + if (this.options["inline"] === true) this.options["inline"] = 3; var pure_funcs = this.options["pure_funcs"]; if (typeof pure_funcs == "function") { this.pure_funcs = pure_funcs; @@ -4030,7 +4031,7 @@ merge(Compressor.prototype, { if (compressor.option("inline") && !fn.uses_arguments && !fn.uses_eval - && fn.body.length == 1 + && (value = can_flatten_body(stat)) && (exp === fn ? !fn.name : compressor.option("unused") && (def = exp.definition()).references.length == 1 @@ -4038,11 +4039,8 @@ merge(Compressor.prototype, { && fn.is_constant_expression(exp.scope)) && !self.pure && !fn.contains_this() - && can_flatten_args(fn) - && (value = flatten_body(stat))) { - var expressions = flatten_args(fn); - expressions.push(value.clone(true)); - return make_sequence(self, expressions).optimize(compressor); + && can_inject_symbols()) { + return make_sequence(self, flatten_fn()).optimize(compressor); } if (compressor.option("side_effects") && all(fn.body, is_empty)) { var args = self.args.concat(make_node(AST_Undefined, self)); @@ -4072,19 +4070,44 @@ merge(Compressor.prototype, { } return self; - function can_flatten_args(fn) { - var catches = Object.create(null); - do { - scope = compressor.parent(++level); - if (scope instanceof AST_Catch) { - catches[scope.argname.name] = true; - } else if (scope instanceof AST_IterationStatement) { - in_loop = []; - } else if (scope instanceof AST_SymbolRef) { - if (scope.fixed_value() instanceof AST_Scope) return false; + function return_value(stat) { + if (!stat) return make_node(AST_Undefined, self); + if (stat instanceof AST_Return) { + if (!stat.value) return make_node(AST_Undefined, self); + return stat.value.clone(true); + } + if (stat instanceof AST_SimpleStatement) { + return make_node(AST_UnaryPrefix, stat, { + operator: "void", + expression: stat.body.clone(true) + }); + } + } + + function can_flatten_body(stat) { + var len = fn.body.length; + if (compressor.option("inline") < 3) { + return len == 1 && return_value(stat); + } + stat = null; + for (var i = 0; i < len; i++) { + var line = fn.body[i]; + if (line instanceof AST_Definitions) { + if (stat && !all(line.definitions, function(var_def) { + return !var_def.value; + })) { + return false; + } + } else if (stat) { + return false; + } else { + stat = line; } - } while (!(scope instanceof AST_Scope)); - var safe_to_inject = compressor.toplevel.vars || !(scope instanceof AST_Toplevel); + } + return return_value(stat); + } + + function can_inject_args(catches, defs, safe_to_inject) { for (var i = 0, len = fn.argnames.length; i < len; i++) { var arg = fn.argnames[i]; if (arg.__unused) continue; @@ -4096,43 +4119,101 @@ merge(Compressor.prototype, { } if (in_loop) in_loop.push(arg.definition()); } - return !in_loop || in_loop.length == 0 || !is_reachable(stat, in_loop); + return true; } - function flatten_args(fn) { - var decls = []; - var expressions = []; + function can_inject_vars(catches, safe_to_inject) { + var len = fn.body.length; + for (var i = 0; i < len; i++) { + var stat = fn.body[i]; + if (!(stat instanceof AST_Definitions)) continue; + if (!safe_to_inject) return false; + for (var j = stat.definitions.length; --j >= 0;) { + var name = stat.definitions[j].name; + if (catches[name.name] + || identifier_atom(name.name) + || scope.var_names()[name.name]) { + return false; + } + } + } + return true; + } + + function can_inject_symbols() { + var catches = Object.create(null); + do { + scope = compressor.parent(++level); + if (scope instanceof AST_Catch) { + catches[scope.argname.name] = true; + } else if (scope instanceof AST_IterationStatement) { + in_loop = []; + } else if (scope instanceof AST_SymbolRef) { + if (scope.fixed_value() instanceof AST_Scope) return false; + } + } while (!(scope instanceof AST_Scope)); + var safe_to_inject = !(scope instanceof AST_Toplevel) || compressor.toplevel.vars; + var inline = compressor.option("inline"); + if (!can_inject_vars(catches, !in_loop && inline >= 3 && safe_to_inject)) return false; + if (!can_inject_args(catches, in_loop, inline >= 2 && safe_to_inject)) return false; + return !in_loop || in_loop.length == 0 || !is_reachable(fn, in_loop); + } + + function append_var(decls, expressions, name, value) { + var def = name.definition(); + scope.var_names()[name.name] = true; + scope.variables.set(name.name, def); + scope.enclosed.push(def); + decls.push(make_node(AST_VarDef, name, { + name: name, + value: null + })); + var sym = make_node(AST_SymbolRef, name, name); + def.references.push(sym); + if (value) expressions.push(make_node(AST_Assign, self, { + operator: "=", + left: sym, + right: value + })); + } + + function flatten_args(decls, expressions) { for (var len = fn.argnames.length, i = len; --i >= 0;) { var name = fn.argnames[i]; var value = self.args[i]; if (name.__unused || scope.var_names()[name.name]) { - if (value) { - expressions.unshift(value); - } + if (value) expressions.push(value); } else { - var def = name.definition(); - 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: symbol, - value: null - })); - var sym = make_node(AST_SymbolRef, name, name); - def.references.push(sym); + name.definition().orig.push(symbol); if (!value && in_loop) value = make_node(AST_Undefined, self); - if (value) expressions.unshift(make_node(AST_Assign, self, { - operator: "=", - left: sym, - right: value - })); + append_var(decls, expressions, symbol, value); } } + decls.reverse(); + expressions.reverse(); for (i = len, len = self.args.length; i < len; i++) { expressions.push(self.args[i]); } + } + + function flatten_body(decls, expressions) { + for (i = 0, len = fn.body.length; i < len; i++) { + var stat = fn.body[i]; + if (stat instanceof AST_Definitions) { + stat.definitions.forEach(function(var_def) { + append_var(decls, expressions, var_def.name, var_def.value); + }); + } + } + expressions.push(value); + } + + function flatten_fn() { + var decls = []; + var expressions = []; + flatten_args(decls, expressions); + flatten_body(decls, expressions); if (decls.length) { i = scope.body.indexOf(compressor.parent(level - 1)) + 1; scope.body.splice(i, 0, make_node(AST_Var, fn, { @@ -4141,17 +4222,6 @@ merge(Compressor.prototype, { } 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/functions.js b/test/compress/functions.js index dd98ba60..f38977b3 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1694,3 +1694,234 @@ loop_init_arg: { } expect_stdout: "PASS" } + +inline_false: { + options = { + inline: false, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_0: { + options = { + inline: 0, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_1: { + options = { + inline: 1, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + console.log(1); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_2: { + options = { + inline: 2, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + console.log(1); + a = 2, console.log(a); + var a; + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_3: { + options = { + inline: 3, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + console.log(1); + a = 2, console.log(a); + var a; + b = 3, c = b, console.log(c); + var b, c; + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_true: { + options = { + inline: true, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + console.log(1); + a = 2, console.log(a); + var a; + b = 3, c = b, console.log(c); + var b, c; + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +use_before_init_in_loop: { + options = { + inline: true, + toplevel: true, + } + input: { + var a = "PASS"; + for (var b = 2; --b >= 0;) (function() { + var c = function() { + return 1; + }(c && (a = "FAIL")); + })(); + console.log(a); + } + expect: { + var a = "PASS"; + for (var b = 2; --b >= 0;) (function() { + var c = (c && (a = "FAIL"), 1); + })(); + console.log(a); + } + expect_stdout: "PASS" +} From 446fb0198bd737c8d34035cc40932ed24ca83bbb Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 3 Jan 2018 04:48:07 +0800 Subject: [PATCH 09/29] extend `__PURE__` to `AST_New` (#2706) fixes #2705 --- lib/parse.js | 6 +- test/compress/pure_funcs.js | 121 ++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 5eb75441..03455348 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1225,12 +1225,14 @@ function parse($TEXT, options) { } else { args = []; } - return subscripts(new AST_New({ + var call = new AST_New({ start : start, expression : newexp, args : args, end : prev() - }), allow_calls); + }); + mark_pure(call); + return subscripts(call, allow_calls); }; function as_atom_node() { diff --git a/test/compress/pure_funcs.js b/test/compress/pure_funcs.js index d15bcca3..0df51e5f 100644 --- a/test/compress/pure_funcs.js +++ b/test/compress/pure_funcs.js @@ -414,3 +414,124 @@ issue_2638: { "/* */(a()||b())(c(),d());", ] } + +issue_2705_1: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/ new a(); + /*@__PURE__*/ (new b()); + new (/*@__PURE__*/ c)(); + (/*@__PURE__*/ new d()); + } + expect_exact: [ + "new/* */c;", + ] +} + +issue_2705_2: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/ new a(1)(2)(3); + /*@__PURE__*/ new (b(1))(2)(3); + /*@__PURE__*/ new (c(1)(2))(3); + /*@__PURE__*/ new (d(1)(2)(3)); + new (/*@__PURE__*/ e)(1)(2)(3); + (/*@__PURE__*/ new f(1))(2)(3); + (/*@__PURE__*/ new g(1)(2))(3); + (/*@__PURE__*/ new h(1)(2)(3)); + } + expect_exact: [ + "new/* */e(1)(2)(3);", + "/* */new f(1)(2)(3);", + "/* */new g(1)(2)(3);", + ] +} + +issue_2705_3: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/ new a.x(1).y(2).z(3); + /*@__PURE__*/ new (b.x)(1).y(2).z(3); + /*@__PURE__*/ new (c.x(1)).y(2).z(3); + /*@__PURE__*/ new (d.x(1).y)(2).z(3); + /*@__PURE__*/ new (e.x(1).y(2)).z(3); + /*@__PURE__*/ new (f.x(1).y(2).z)(3); + /*@__PURE__*/ new (g.x(1).y(2).z(3)); + new (/*@__PURE__*/ h).x(1).y(2).z(3); + /* */ new (/*@__PURE__*/ i.x)(1).y(2).z(3); + (/*@__PURE__*/ new j.x(1)).y(2).z(3); + (/*@__PURE__*/ new k.x(1).y)(2).z(3); + (/*@__PURE__*/ new l.x(1).y(2)).z(3); + (/*@__PURE__*/ new m.x(1).y(2).z)(3); + (/*@__PURE__*/ new n.x(1).y(2).z(3)); + } + expect_exact: [ + "new/* */h.x(1).y(2).z(3);", + "/* */new/* */i.x(1).y(2).z(3);", + "/* */new j.x(1).y(2).z(3);", + "/* */new k.x(1).y(2).z(3);", + "/* */new l.x(1).y(2).z(3);", + "/* */new m.x(1).y(2).z(3);", + ] +} + +issue_2705_4: { + options = { + side_effects: true, + } + input: { + (/*@__PURE__*/ new x(), y()); + (w(), /*@__PURE__*/ new x(), y()); + } + expect: { + y(); + w(), y(); + } +} + +issue_2705_5: { + options = { + side_effects: true, + } + input: { + [ /*@__PURE__*/ new x() ]; + [ /*@__PURE__*/ new x(), y() ]; + [ w(), /*@__PURE__*/ new x(), y() ]; + } + expect: { + y(); + w(), y(); + } +} + +issue_2705_6: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/new (g() || h())(x(), y()); + /* */ new (/*@__PURE__*/ (a() || b()))(c(), d()); + } + expect_exact: [ + "/* */x(),y();", + "/* */new(/* */a()||b())(c(),d());", + ] +} From 14778e049b12e131fc05ddacff9cda56dfede77d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 3 Jan 2018 17:18:38 +0800 Subject: [PATCH 10/29] fix `reduce_vars` on `AST_Defun` (#2708) --- lib/compress.js | 17 +-- lib/scope.js | 25 ++-- test/compress/reduce_vars.js | 238 +++++++++++++++++++++++++++++++++++ test/compress/typeof.js | 2 +- 4 files changed, 257 insertions(+), 25 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 06873df8..1e096ade 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -316,7 +316,7 @@ merge(Compressor.prototype, { if (def.scope.uses_eval || def.scope.uses_with) { def.fixed = false; } else if (!compressor.exposed(def)) { - def.fixed = undefined; + def.fixed = def.init; } else { def.fixed = false; } @@ -329,10 +329,12 @@ merge(Compressor.prototype, { function reset_variables(tw, compressor, node) { node.variables.each(function(def) { reset_def(compressor, def); - if (def.fixed === undefined && def.orig[0].TYPE == "SymbolVar") { - def.fixed = null; + if (def.fixed === null) { def.safe_ids = tw.safe_ids; mark(tw, def, true); + } else if (def.fixed) { + tw.loop_ids[def.id] = tw.in_loop; + mark(tw, def, true); } }); } @@ -504,15 +506,6 @@ merge(Compressor.prototype, { }); def(AST_Defun, function(tw, descend, compressor) { 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); reset_variables(tw, compressor, this); diff --git a/lib/scope.js b/lib/scope.js index bceec289..79b2475d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -43,9 +43,10 @@ "use strict"; -function SymbolDef(scope, orig) { +function SymbolDef(scope, orig, init) { this.name = orig.name; this.orig = [ orig ]; + this.init = init; this.eliminated = 0; this.scope = scope; this.references = []; @@ -158,10 +159,10 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // scope when we encounter the AST_Defun node (which is // instanceof AST_Scope) but we get to the symbol a bit // later. - (node.scope = defun.parent_scope).def_function(node); + (node.scope = defun.parent_scope).def_function(node, defun); } else if (node instanceof AST_SymbolVar) { - defun.def_variable(node); + defun.def_variable(node, node.TYPE == "SymbolVar" ? null : undefined); if (defun !== scope) { node.mark_enclosed(options); var def = scope.find_variable(node); @@ -306,21 +307,21 @@ AST_Scope.DEFMETHOD("find_variable", function(name){ || (this.parent_scope && this.parent_scope.find_variable(name)); }); -AST_Scope.DEFMETHOD("def_function", function(symbol){ - var def = this.def_variable(symbol); +AST_Scope.DEFMETHOD("def_function", function(symbol, init){ + var def = this.def_variable(symbol, init); + if (!def.init) def.init = init; this.functions.set(symbol.name, def); return def; }); -AST_Scope.DEFMETHOD("def_variable", function(symbol){ - var def; - if (!this.variables.has(symbol.name)) { - def = new SymbolDef(this, symbol); +AST_Scope.DEFMETHOD("def_variable", function(symbol, init){ + var def = this.variables.get(symbol.name); + if (def) { + def.orig.push(symbol); + } else { + def = new SymbolDef(this, symbol, init); this.variables.set(symbol.name, def); def.global = !this.parent_scope; - } else { - def = this.variables.get(symbol.name); - def.orig.push(symbol); } return symbol.thedef = def; }); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 058e9dde..e370e5be 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -4999,3 +4999,241 @@ var_if: { } } } + +defun_assign: { + options = { + reduce_vars: true, + toplevel: true, + } + input: { + console.log(typeof a); + a = 42; + console.log(typeof a); + function a() {} + console.log(typeof a); + } + expect: { + console.log(typeof a); + a = 42; + console.log(typeof a); + function a() {} + console.log(typeof a); + } + expect_stdout: [ + "function", + "number", + "number", + ] +} + +defun_var_1: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + typeofs: true, + unused: true, + } + input: { + var a = 42, b; + function a() {} + function b() {} + console.log(typeof a, typeof b); + } + expect: { + var a = 42; + function a() {} + console.log(typeof a, "function"); + } + expect_stdout: "number function" +} + +defun_var_2: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + typeofs: true, + unused: true, + } + input: { + function a() {} + function b() {} + var a = 42, b; + console.log(typeof a, typeof b); + } + expect: { + function a() {} + var a = 42; + console.log(typeof a, "function"); + } + expect_stdout: "number function" +} + +defun_var_3: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + typeofs: true, + unused: true, + } + input: { + function a() {} + function b() {} + console.log(typeof a, typeof b); + var a = 42, b; + } + expect: { + function a() {} + console.log(typeof a, "function"); + var a = 42; + } + expect_stdout: "function function" +} + +defun_catch_1: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function a() {} + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect_stdout: "42" +} + +defun_catch_2: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + function a() {} + throw 42; + } catch (a) { + console.log(a); + } + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect_stdout: "42" +} + +defun_catch_3: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + throw 42; + function a() {} + } catch (a) { + console.log(a); + } + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect_stdout: "42" +} + +defun_catch_4: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + throw 42; + } catch (a) { + function a() {} + console.log(a); + } + } + expect: { + try { + throw 42; + } catch (a) { + function a() {} + console.log(a); + } + } + expect_stdout: true +} + +defun_catch_5: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + throw 42; + } catch (a) { + console.log(a); + function a() {} + } + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + function a() {} + } + } + expect_stdout: true +} + +defun_catch_6: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + throw 42; + } catch (a) { + console.log(a); + } + function a() {} + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect_stdout: "42" +} diff --git a/test/compress/typeof.js b/test/compress/typeof.js index 180e5451..9eaf05e4 100644 --- a/test/compress/typeof.js +++ b/test/compress/typeof.js @@ -100,7 +100,7 @@ typeof_defun_1: { g = 42; console.log("YES"); "function" == typeof g && g(); - h(); + "function" == typeof h && h(); } expect_stdout: [ "YES", From cfe3a98ce50a1eb844654da57b4ef47a750feda5 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 4 Jan 2018 01:03:33 +0800 Subject: [PATCH 11/29] drop `unused` assignment based on `reduce_vars` (#2709) --- lib/compress.js | 45 +++++++++---- lib/scope.js | 5 +- test/compress/drop-unused.js | 122 +++++++++++++++++++++++++++++++++++ test/compress/reduce_vars.js | 41 +++++++++++- test/compress/typeof.js | 40 ++++++++++++ 5 files changed, 238 insertions(+), 15 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 1e096ade..fd59fb32 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -311,6 +311,7 @@ merge(Compressor.prototype, { def(AST_Node, noop); function reset_def(compressor, def) { + def.assignments = 0; def.direct_access = false; def.escaped = false; if (def.scope.uses_eval || def.scope.uses_with) { @@ -364,6 +365,7 @@ merge(Compressor.prototype, { } function safe_to_assign(tw, def, value) { + if (def.fixed === undefined) return true; if (def.fixed === null && def.safe_ids) { def.safe_ids[def.id] = false; delete def.safe_ids; @@ -372,7 +374,7 @@ merge(Compressor.prototype, { 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; + if (def.fixed != null && (!value || def.references.length > def.assignments)) return false; return all(def.orig, function(sym) { return !(sym instanceof AST_SymbolDefun || sym instanceof AST_SymbolLambda); @@ -477,6 +479,7 @@ merge(Compressor.prototype, { var d = node.left.definition(); if (safe_to_assign(tw, d, node.right)) { d.references.push(node.left); + d.assignments++; d.fixed = function() { return node.right; }; @@ -662,7 +665,7 @@ merge(Compressor.prototype, { 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 (safe_to_assign(tw, d, node.value)) { if (node.value) { d.fixed = function() { return node.value; @@ -2717,6 +2720,7 @@ merge(Compressor.prototype, { }; var in_use = []; var in_use_ids = Object.create(null); // avoid expensive linear scans of in_use + var fixed_ids = Object.create(null); if (self instanceof AST_Toplevel && compressor.top_retain) { self.variables.each(function(def) { if (compressor.top_retain(def) && !(def.id in in_use_ids)) { @@ -2763,6 +2767,9 @@ merge(Compressor.prototype, { if (def.value.has_side_effects(compressor)) { def.value.walk(tw); } + if (def.name.fixed_value() === def.value) { + fixed_ids[node_def.id] = true; + } } }); return true; @@ -2786,12 +2793,16 @@ merge(Compressor.prototype, { var parent = tt.parent(); if (drop_vars) { var sym = assign_as_unused(node); - if (sym instanceof AST_SymbolRef - && !(sym.definition().id in in_use_ids)) { + if (sym instanceof AST_SymbolRef) { + var def = sym.definition(); + var in_use = def.id in in_use_ids; if (node instanceof AST_Assign) { - return maintain_this_binding(parent, node, node.right.transform(tt)); - } - return make_node(AST_Number, node, { + if (!in_use + || def.id in fixed_ids + && node.left.fixed_value() !== node.right) { + return maintain_this_binding(parent, node, node.right.transform(tt)); + } + } else if (!in_use) return make_node(AST_Number, node, { value: 0 }); } @@ -2851,13 +2862,16 @@ merge(Compressor.prototype, { operator: "=", left: make_node(AST_SymbolRef, def.name, def.name), right: def.value - })); + }).transform(tt)); } remove(var_defs, def); sym.eliminated++; return; } } + if (def.value && sym.id in fixed_ids && def.name.fixed_value() !== def.value) { + def.value = def.value.drop_side_effect_free(compressor); + } if (def.value) { if (side_effects.length > 0) { if (tail.length > 0) { @@ -2962,14 +2976,19 @@ merge(Compressor.prototype, { self.transform(tt); function scan_ref_scoped(node, descend) { - var sym; - if ((sym = assign_as_unused(node)) instanceof AST_SymbolRef - && self.variables.get(sym.name) === sym.definition()) { - if (node instanceof AST_Assign) node.right.walk(tw); + var node_def, sym = assign_as_unused(node); + if (sym instanceof AST_SymbolRef + && self.variables.get(sym.name) === (node_def = sym.definition())) { + if (node instanceof AST_Assign) { + node.right.walk(tw); + if (node.left.fixed_value() === node.right) { + fixed_ids[node_def.id] = true; + } + } return true; } if (node instanceof AST_SymbolRef) { - var node_def = node.definition(); + node_def = node.definition(); if (!(node_def.id in in_use_ids)) { in_use_ids[node_def.id] = true; in_use.push(node_def); diff --git a/lib/scope.js b/lib/scope.js index 79b2475d..de92fc94 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -151,7 +151,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.references = []; } if (node instanceof AST_SymbolLambda) { - defun.def_function(node); + defun.def_function(node, defun); } else if (node instanceof AST_SymbolDefun) { // Careful here, the scope where this should be defined is @@ -318,6 +318,9 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol, init){ var def = this.variables.get(symbol.name); if (def) { def.orig.push(symbol); + if (def.init && (def.scope !== symbol.scope || def.init instanceof AST_Function)) { + def.init = init; + } } else { def = new SymbolDef(this, symbol, init); this.variables.set(symbol.name, def); diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 21d4b7ce..5ad489b9 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -1522,3 +1522,125 @@ issue_2665: { } expect_stdout: "-1" } + +double_assign_1: { + options = { + passes: 2, + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f1() { + var a = {}; + var a = []; + return a; + } + function f2() { + var a = {}; + a = []; + return a; + } + function f3() { + a = {}; + var a = []; + return a; + } + function f4(a) { + a = {}; + a = []; + return a; + } + function f5(a) { + var a = {}; + a = []; + return a; + } + function f6(a) { + a = {}; + var a = []; + return a; + } + console.log(f1(), f2(), f3(), f4(), f5(), f6()); + } + expect: { + function f1() { + return []; + } + function f2() { + var a; + a = []; + return a; + } + function f3() { + return []; + } + function f4(a) { + a = []; + return a; + } + function f5(a) { + a = []; + return a; + } + function f6(a) { + a = []; + return a; + } + console.log(f1(), f2(), f3(), f4(), f5(), f6()); + } + expect_stdout: true +} + +double_assign_2: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + for (var i = 0; i < 2; i++) + a = void 0, a = {}, console.log(a); + var a; + } + expect: { + for (var i = 0; i < 2; i++) + void 0, a = {}, console.log(a); + var a; + } +} + +double_assign_3: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + for (var i = 0; i < 2; i++) + a = void 0, a = { a: a }, console.log(a); + var a; + } + expect: { + for (var i = 0; i < 2; i++) + a = void 0, a = { a: a }, console.log(a); + var a; + } +} + +cascade_drop_assign: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var a, b = a = "PASS"; + console.log(b); + } + expect: { + var b = "PASS"; + console.log(b); + } + expect_stdout: "PASS" +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index e370e5be..3d993b90 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -299,7 +299,7 @@ unsafe_evaluate_modified: { console.log(function(){ var o={p:1}; o.p++; console.log(o.p); return o.p; }()); console.log(function(){ var o={p:2}; --o.p; console.log(o.p); return o.p; }()); console.log(function(){ var o={p:3}; o.p += ""; console.log(o.p); return o.p; }()); - console.log(function(){ var o={p:4}; o = {}; console.log(o.p); return o.p; }()); + console.log(function(){ var o; o = {}; console.log(o.p); return o.p; }()); console.log(function(){ var o={p:5}; o.p = -9; console.log(o.p); return o.p; }()); function inc() { this.p++; } console.log(function(){ var o={p:6}; inc.call(o); console.log(o.p); return o.p; }()); @@ -5237,3 +5237,42 @@ defun_catch_6: { } expect_stdout: "42" } + +duplicate_lambda_defun_name_1: { + options = { + reduce_vars: true, + } + input: { + console.log(function f(a) { + function f() {} + return f.length; + }()); + } + expect: { + console.log(function f(a) { + function f() {} + return f.length; + }()); + } + expect_stdout: "0" +} + +duplicate_lambda_defun_name_2: { + options = { + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + console.log(function f(a) { + function f() {} + return f.length; + }()); + } + expect: { + console.log(function(a) { + return function() {}.length; + }()); + } + expect_stdout: "0" +} diff --git a/test/compress/typeof.js b/test/compress/typeof.js index 9eaf05e4..72e77beb 100644 --- a/test/compress/typeof.js +++ b/test/compress/typeof.js @@ -138,3 +138,43 @@ typeof_defun_2: { "2", ] } + +duplicate_defun_arg_name: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + function long_name(long_name) { + return typeof long_name; + } + console.log(typeof long_name, long_name()); + } + expect: { + function long_name(long_name) { + return typeof long_name; + } + console.log(typeof long_name, long_name()); + } + expect_stdout: "function undefined" +} + +duplicate_lambda_arg_name: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + console.log(function long_name(long_name) { + return typeof long_name; + }()); + } + expect: { + console.log(function long_name(long_name) { + return typeof long_name; + }()); + } + expect_stdout: "undefined" +} From c598a12af956a939587059f0f4753a0631b5b372 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 4 Jan 2018 03:18:28 +0800 Subject: [PATCH 12/29] apply `collapse_vars` to loop conditions (#2712) --- lib/compress.js | 5 +++++ test/compress/functions.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index fd59fb32..300bc61d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1171,10 +1171,14 @@ merge(Compressor.prototype, { extract_candidates(expr.alternative); } else if (expr instanceof AST_Definitions) { expr.definitions.forEach(extract_candidates); + } else if (expr instanceof AST_DWLoop) { + extract_candidates(expr.condition); } 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); + if (expr.condition) extract_candidates(expr.condition); + if (expr.step) extract_candidates(expr.step); } else if (expr instanceof AST_If) { extract_candidates(expr.condition); } else if (expr instanceof AST_Sequence) { @@ -1205,6 +1209,7 @@ merge(Compressor.prototype, { 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_IterationStatement) return node; if (parent instanceof AST_Sequence) return find_stop(parent, level + 1); if (parent instanceof AST_Switch) return node; if (parent instanceof AST_VarDef) return node; diff --git a/test/compress/functions.js b/test/compress/functions.js index f38977b3..b6b68982 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1416,7 +1416,7 @@ issue_2630_5: { !function() { do { c *= 10; - } while (c += 3, (c = 2 + c) < 100); + } while ((c = 2 + (c += 3)) < 100); }(); console.log(c); } From 9b58b54e2d7ce4ab2c015611add52c7475de9eab Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 4 Jan 2018 12:58:40 +0800 Subject: [PATCH 13/29] extend `inline` (#2714) - compress `function` with variables within loops - restrict to `AST_Var` for better compatibility with ES6+ --- lib/compress.js | 57 +++++++++++++++++++++++--------------- test/compress/functions.js | 8 ++++-- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 300bc61d..85bb0910 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4109,7 +4109,7 @@ merge(Compressor.prototype, { stat = null; for (var i = 0; i < len; i++) { var line = fn.body[i]; - if (line instanceof AST_Definitions) { + if (line instanceof AST_Var) { if (stat && !all(line.definitions, function(var_def) { return !var_def.value; })) { @@ -4124,7 +4124,7 @@ merge(Compressor.prototype, { return return_value(stat); } - function can_inject_args(catches, defs, safe_to_inject) { + function can_inject_args(catches, safe_to_inject) { for (var i = 0, len = fn.argnames.length; i < len; i++) { var arg = fn.argnames[i]; if (arg.__unused) continue; @@ -4143,7 +4143,7 @@ merge(Compressor.prototype, { var len = fn.body.length; for (var i = 0; i < len; i++) { var stat = fn.body[i]; - if (!(stat instanceof AST_Definitions)) continue; + if (!(stat instanceof AST_Var)) continue; if (!safe_to_inject) return false; for (var j = stat.definitions.length; --j >= 0;) { var name = stat.definitions[j].name; @@ -4152,6 +4152,7 @@ merge(Compressor.prototype, { || scope.var_names()[name.name]) { return false; } + if (in_loop) in_loop.push(name.definition()); } } return true; @@ -4171,8 +4172,8 @@ merge(Compressor.prototype, { } while (!(scope instanceof AST_Scope)); var safe_to_inject = !(scope instanceof AST_Toplevel) || compressor.toplevel.vars; var inline = compressor.option("inline"); - if (!can_inject_vars(catches, !in_loop && inline >= 3 && safe_to_inject)) return false; - if (!can_inject_args(catches, in_loop, inline >= 2 && safe_to_inject)) return false; + if (!can_inject_vars(catches, inline >= 3 && safe_to_inject)) return false; + if (!can_inject_args(catches, inline >= 2 && safe_to_inject)) return false; return !in_loop || in_loop.length == 0 || !is_reachable(fn, in_loop); } @@ -4181,13 +4182,13 @@ merge(Compressor.prototype, { scope.var_names()[name.name] = true; scope.variables.set(name.name, def); scope.enclosed.push(def); - decls.push(make_node(AST_VarDef, name, { + decls.unshift(make_node(AST_VarDef, name, { name: name, value: null })); var sym = make_node(AST_SymbolRef, name, name); def.references.push(sym); - if (value) expressions.push(make_node(AST_Assign, self, { + if (value) expressions.unshift(make_node(AST_Assign, self, { operator: "=", left: sym, right: value @@ -4195,11 +4196,15 @@ merge(Compressor.prototype, { } function flatten_args(decls, expressions) { - for (var len = fn.argnames.length, i = len; --i >= 0;) { + var len = fn.argnames.length; + for (var i = self.args.length; --i >= len;) { + expressions.unshift(self.args[i]); + } + for (i = len; --i >= 0;) { var name = fn.argnames[i]; var value = self.args[i]; if (name.__unused || scope.var_names()[name.name]) { - if (value) expressions.push(value); + if (value) expressions.unshift(value); } else { var symbol = make_node(AST_SymbolVar, name, name); name.definition().orig.push(symbol); @@ -4207,30 +4212,38 @@ merge(Compressor.prototype, { append_var(decls, expressions, symbol, value); } } - decls.reverse(); - expressions.reverse(); - for (i = len, len = self.args.length; i < len; i++) { - expressions.push(self.args[i]); - } } - function flatten_body(decls, expressions) { - for (i = 0, len = fn.body.length; i < len; i++) { + function flatten_vars(decls, expressions) { + if (in_loop) in_loop.length = 0; + for (var i = fn.body.length; --i >= 0;) { var stat = fn.body[i]; - if (stat instanceof AST_Definitions) { - stat.definitions.forEach(function(var_def) { - append_var(decls, expressions, var_def.name, var_def.value); - }); + if (!(stat instanceof AST_Var)) continue; + for (var j = stat.definitions.length; --j >= 0;) { + var var_def = stat.definitions[j]; + var name = var_def.name; + append_var(decls, expressions, name, var_def.value); + if (in_loop) { + var def = name.definition(); + var sym = make_node(AST_SymbolRef, name, name); + def.references.push(sym); + in_loop.unshift(make_node(AST_Assign, var_def, { + operator: "=", + left: sym, + right: make_node(AST_Undefined, name) + })); + } } } - expressions.push(value); + if (in_loop) [].unshift.apply(expressions, in_loop); } function flatten_fn() { var decls = []; var expressions = []; + flatten_vars(decls, expressions); flatten_args(decls, expressions); - flatten_body(decls, expressions); + expressions.push(value); if (decls.length) { i = scope.body.indexOf(compressor.parent(level - 1)) + 1; scope.body.splice(i, 0, make_node(AST_Var, fn, { diff --git a/test/compress/functions.js b/test/compress/functions.js index b6b68982..ff3baeb2 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1299,6 +1299,7 @@ issue_2630_2: { passes: 2, reduce_vars: true, sequences: true, + side_effects: true, unused: true, } input: { @@ -1905,6 +1906,7 @@ inline_true: { use_before_init_in_loop: { options = { inline: true, + side_effects: true, toplevel: true, } input: { @@ -1918,9 +1920,9 @@ use_before_init_in_loop: { } expect: { var a = "PASS"; - for (var b = 2; --b >= 0;) (function() { - var c = (c && (a = "FAIL"), 1); - })(); + for (var b = 2; --b >= 0;) + c = void 0, c = (c && (a = "FAIL"), 1); + var c; console.log(a); } expect_stdout: "PASS" From 7a6d452b548c8b7783b226f646e814d6cb0cf32b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 4 Jan 2018 15:53:53 +0800 Subject: [PATCH 14/29] preserve constant modification under strict mode (#2717) --- lib/compress.js | 7 ++++- test/compress/pure_getters.js | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 85bb0910..fe504164 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3279,7 +3279,12 @@ merge(Compressor.prototype, { }); def(AST_Assign, function(compressor){ var left = this.left; - if (left.has_side_effects(compressor)) return this; + if (left.has_side_effects(compressor) + || compressor.has_directive("use strict") + && left instanceof AST_PropAccess + && left.expression.is_constant()) { + return this; + } this.write_only = true; while (left instanceof AST_PropAccess) { left = left.expression; diff --git a/test/compress/pure_getters.js b/test/compress/pure_getters.js index 7185e0c6..80b0e8ea 100644 --- a/test/compress/pure_getters.js +++ b/test/compress/pure_getters.js @@ -348,6 +348,57 @@ set_immutable_4: { expect_stdout: true } +set_immutable_5: { + options = { + collapse_vars: true, + conditionals: true, + evaluate: true, + pure_getters: "strict", + reduce_funcs: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + "use strict"; + var a = 1; + a.foo += ""; + if (a.foo) console.log("FAIL"); + else console.log("PASS"); + } + expect: { + "use strict"; + 1..foo += ""; + 1..foo ? console.log("FAIL") : console.log("PASS"); + } + expect_stdout: true +} + +set_immutable_6: { + options = { + collapse_vars: true, + conditionals: true, + evaluate: true, + pure_getters: "strict", + reduce_funcs: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a = 1; + a.foo += ""; + if (a.foo) console.log("FAIL"); + else console.log("PASS"); + } + expect: { + 1..foo ? console.log("FAIL") : console.log("PASS"); + } + expect_stdout: true +} + set_mutable_1: { options = { collapse_vars: true, From a6873a38590a9176c607bcdbff726daa93e1fec7 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 4 Jan 2018 18:45:51 +0800 Subject: [PATCH 15/29] forbid block-scoped `AST_Defun` in strict mode (#2718) --- lib/compress.js | 2 +- lib/parse.js | 15 +++-- test/compress/dead-code.js | 76 --------------------- test/compress/functions.js | 40 ----------- test/run-tests.js | 133 +++++++++++++++++++------------------ 5 files changed, 79 insertions(+), 187 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index fe504164..481af929 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1746,7 +1746,7 @@ merge(Compressor.prototype, { target.push(node); return true; } - if (node instanceof AST_Defun && (node === stat || !compressor.has_directive("use strict"))) { + if (node instanceof AST_Defun) { target.push(node); return true; } diff --git a/lib/parse.js b/lib/parse.js index 03455348..001587bc 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -800,7 +800,7 @@ function parse($TEXT, options) { function embed_tokens(parser) { return function() { var start = S.token; - var expr = parser(); + var expr = parser.apply(null, arguments); var end = prev(); expr.start = start; expr.end = end; @@ -815,7 +815,7 @@ function parse($TEXT, options) { } }; - var statement = embed_tokens(function() { + var statement = embed_tokens(function(strict_defun) { handle_regexp(); switch (S.token.type) { case "string": @@ -901,6 +901,9 @@ function parse($TEXT, options) { return for_(); case "function": + if (!strict_defun && S.input.has_directive("use strict")) { + croak("In strict mode code, functions can only be declared at top level or immediately within another function."); + } next(); return function_(AST_Defun); @@ -1083,7 +1086,7 @@ function parse($TEXT, options) { S.input.push_directives_stack(); S.in_loop = 0; S.labels = []; - var body = block_(); + var body = block_(true); if (S.input.has_directive("use strict")) { if (name) strict_verify_symbol(name); argnames.forEach(strict_verify_symbol); @@ -1112,12 +1115,12 @@ function parse($TEXT, options) { }); }; - function block_() { + function block_(strict_defun) { expect("{"); var a = []; while (!is("punc", "}")) { if (is("eof")) unexpected(); - a.push(statement()); + a.push(statement(strict_defun)); } next(); return a; @@ -1630,7 +1633,7 @@ function parse($TEXT, options) { var body = []; S.input.push_directives_stack(); while (!is("eof")) - body.push(statement()); + body.push(statement(true)); S.input.pop_directives_stack(); var end = prev(); var toplevel = options.toplevel; diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 32bb88e6..490cff7a 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -62,46 +62,6 @@ dead_code_2_should_warn: { node_version: "<=4" } -dead_code_2_should_warn_strict: { - options = { - dead_code: true - }; - input: { - "use strict"; - function f() { - g(); - x = 10; - throw new Error("foo"); - // completely discarding the `if` would introduce some - // bugs. UglifyJS v1 doesn't deal with this issue; in v2 - // we copy any declarations to the upper scope. - if (x) { - y(); - var x; - function g(){}; - // but nested declarations should not be kept. - (function(){ - var q; - function y(){}; - })(); - } - } - f(); - } - expect: { - "use strict"; - function f() { - g(); - x = 10; - throw new Error("foo"); - var x; - } - f(); - } - expect_stdout: true - node_version: ">=4" -} - dead_code_constant_boolean_should_warn_more: { options = { dead_code : true, @@ -137,42 +97,6 @@ dead_code_constant_boolean_should_warn_more: { node_version: "<=4" } -dead_code_constant_boolean_should_warn_more_strict: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true, - side_effects : true, - }; - input: { - "use strict"; - while (!((foo && bar) || (x + "0"))) { - console.log("unreachable"); - var foo; - function bar() {} - } - for (var x = 10, y; x && (y || x) && (!typeof x); ++x) { - asdf(); - foo(); - var moo; - } - bar(); - } - expect: { - "use strict"; - var foo; - // nothing for the while - // as for the for, it should keep: - var moo; - var x = 10, y; - bar(); - } - expect_stdout: true - node_version: ">=4" -} - try_catch_finally: { options = { conditionals: true, diff --git a/test/compress/functions.js b/test/compress/functions.js index ff3baeb2..5b0c49b8 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -214,46 +214,6 @@ hoist_funs: { node_version: "<=4" } -hoist_funs_strict: { - options = { - hoist_funs: true, - } - input: { - "use strict"; - console.log(1, typeof f, typeof g); - if (console.log(2, typeof f, typeof g)) - console.log(3, typeof f, typeof g); - else { - console.log(4, typeof f, typeof g); - function f() {} - console.log(5, typeof f, typeof g); - } - function g() {} - console.log(6, typeof f, typeof g); - } - expect: { - "use strict"; - function g() {} - console.log(1, typeof f, typeof g); - if (console.log(2, typeof f, typeof g)) - console.log(3, typeof f, typeof g); - else { - console.log(4, typeof f, typeof g); - function f() {} - console.log(5, typeof f, typeof g); - } - console.log(6, typeof f, typeof g); - } - expect_stdout: [ - "1 'undefined' 'function'", - "2 'undefined' 'function'", - "4 'function' 'function'", - "5 'function' 'function'", - "6 'undefined' 'function'", - ] - node_version: ">=4" -} - issue_203: { options = { keep_fargs: false, diff --git a/test/run-tests.js b/test/run-tests.js index e95bbb83..e5f79678 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -100,6 +100,15 @@ function run_compress_tests() { quote_style: 3, keep_quoted_props: true }); + try { + U.parse(input_code); + } catch (ex) { + log("!!! Cannot parse input\n---INPUT---\n{input}\n--PARSE ERROR--\n{error}\n\n", { + input: input_formatted, + error: ex, + }); + return false; + } var options = U.defaults(test.options, { warnings: false }); @@ -139,78 +148,74 @@ function run_compress_tests() { output: output, expected: expect }); - failures++; - failed_files[file] = 1; + return false; } - else { - // expect == output - try { - var reparsed_ast = U.parse(output); - } catch (ex) { - log("!!! Test matched expected result but cannot parse output\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--REPARSE ERROR--\n{error}\n\n", { + // expect == output + try { + U.parse(output); + } catch (ex) { + log("!!! Test matched expected result but cannot parse output\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--REPARSE ERROR--\n{error}\n\n", { + input: input_formatted, + output: output, + error: ex, + }); + return false; + } + if (test.expect_warnings) { + U.AST_Node.warn_function = original_warn_function; + var expected_warnings = make_code(test.expect_warnings, { + beautify: false, + quote_style: 2, // force double quote to match JSON + }); + warnings_emitted = warnings_emitted.map(function(input) { + return input.split(process.cwd() + path.sep).join("").split(path.sep).join("/"); + }); + var actual_warnings = JSON.stringify(warnings_emitted); + if (expected_warnings != actual_warnings) { + log("!!! failed\n---INPUT---\n{input}\n---EXPECTED WARNINGS---\n{expected_warnings}\n---ACTUAL WARNINGS---\n{actual_warnings}\n\n", { input: input_formatted, - output: output, - error: ex.toString(), + expected_warnings: expected_warnings, + actual_warnings: actual_warnings, }); - failures++; - failed_files[file] = 1; - } - if (test.expect_warnings) { - U.AST_Node.warn_function = original_warn_function; - var expected_warnings = make_code(test.expect_warnings, { - beautify: false, - quote_style: 2, // force double quote to match JSON - }); - warnings_emitted = warnings_emitted.map(function(input) { - return input.split(process.cwd() + path.sep).join("").split(path.sep).join("/"); - }); - var actual_warnings = JSON.stringify(warnings_emitted); - if (expected_warnings != actual_warnings) { - log("!!! failed\n---INPUT---\n{input}\n---EXPECTED WARNINGS---\n{expected_warnings}\n---ACTUAL WARNINGS---\n{actual_warnings}\n\n", { - input: input_formatted, - expected_warnings: expected_warnings, - actual_warnings: actual_warnings, - }); - failures++; - failed_files[file] = 1; - } - } - if (test.expect_stdout - && (!test.node_version || semver.satisfies(process.version, test.node_version))) { - var stdout = sandbox.run_code(input_code); - if (test.expect_stdout === true) { - test.expect_stdout = stdout; - } - if (!sandbox.same_stdout(test.expect_stdout, stdout)) { - log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { - input: input_formatted, - expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", - expected: test.expect_stdout, - actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", - actual: stdout, - }); - failures++; - failed_files[file] = 1; - } else { - stdout = sandbox.run_code(output); - if (!sandbox.same_stdout(test.expect_stdout, stdout)) { - log("!!! failed\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { - input: input_formatted, - expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", - expected: test.expect_stdout, - actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", - actual: stdout, - }); - failures++; - failed_files[file] = 1; - } - } + return false; } } + if (test.expect_stdout + && (!test.node_version || semver.satisfies(process.version, test.node_version))) { + var stdout = sandbox.run_code(input_code); + if (test.expect_stdout === true) { + test.expect_stdout = stdout; + } + if (!sandbox.same_stdout(test.expect_stdout, stdout)) { + log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { + input: input_formatted, + expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", + expected: test.expect_stdout, + actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", + actual: stdout, + }); + return false; + } + stdout = sandbox.run_code(output); + if (!sandbox.same_stdout(test.expect_stdout, stdout)) { + log("!!! failed\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { + input: input_formatted, + expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", + expected: test.expect_stdout, + actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", + actual: stdout, + }); + return false; + } + } + return true; } var tests = parse_test(path.resolve(dir, file)); for (var i in tests) if (tests.hasOwnProperty(i)) { - test_case(tests[i]); + if (!test_case(tests[i])) { + failures++; + failed_files[file] = 1; + } } } files.forEach(function(file){ From 6f3f21233f61fbb014f5e5029baa1e4a8a91e0e7 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 4 Jan 2018 20:13:05 +0800 Subject: [PATCH 16/29] reminify tests upon `expect_stdout` (#2716) --- test/run-tests.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/run-tests.js b/test/run-tests.js index e5f79678..5dcacd87 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -10,6 +10,7 @@ var semver = require("semver"); var tests_dir = path.dirname(module.filename); var failures = 0; var failed_files = {}; +var minify_options = require("./ufuzz.json").map(JSON.stringify); run_compress_tests(); if (failures) { @@ -207,6 +208,9 @@ function run_compress_tests() { }); return false; } + if (!reminify(test.options, input_code, input_formatted, test.expect_stdout)) { + return false; + } } return true; } @@ -351,3 +355,46 @@ function evaluate(code) { code = make_code(code, { beautify: true }); return new Function("return(" + code + ")")(); } + +// Try to reminify original input with standard options +// to see if it matches expect_stdout. +function reminify(orig_options, input_code, input_formatted, expect_stdout) { + for (var i = 0; i < minify_options.length; i++) { + var options = JSON.parse(minify_options[i]); + if (options.compress) [ + "keep_fargs", + "keep_fnames", + ].forEach(function(name) { + if (name in orig_options) { + options.compress[name] = orig_options[name]; + } + }); + var options_formatted = JSON.stringify(options, null, 4); + var result = U.minify(input_code, options); + if (result.error) { + log("!!! failed input reminify\n---INPUT---\n{input}\n--ERROR---\n{error}\n\n", { + input: input_formatted, + error: result.error, + }); + return false; + } else { + var stdout = sandbox.run_code(result.code); + if (typeof expect_stdout != "string" && typeof stdout != "string" && expect_stdout.name == stdout.name) { + stdout = expect_stdout; + } + if (!sandbox.same_stdout(expect_stdout, stdout)) { + log("!!! failed running reminified input\n---INPUT---\n{input}\n---OPTIONS---\n{options}\n---OUTPUT---\n{output}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { + input: input_formatted, + options: options_formatted, + output: result.code, + expected_type: typeof expect_stdout == "string" ? "STDOUT" : "ERROR", + expected: expect_stdout, + actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", + actual: stdout, + }); + return false; + } + } + } + return true; +} From 484e484571448595389e4c6fd9559e47c5591f7a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 4 Jan 2018 23:38:37 +0800 Subject: [PATCH 17/29] fix corner case in `inline` (#2720) --- lib/compress.js | 31 +++++++++++++++++-------------- test/compress/functions.js | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 481af929..3a3aac4f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4184,16 +4184,18 @@ merge(Compressor.prototype, { function append_var(decls, expressions, name, value) { 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 - })); + if (!scope.var_names()[name.name]) { + scope.var_names()[name.name] = true; + decls.push(make_node(AST_VarDef, name, { + name: name, + value: null + })); + } var sym = make_node(AST_SymbolRef, name, name); def.references.push(sym); - if (value) expressions.unshift(make_node(AST_Assign, self, { + if (value) expressions.push(make_node(AST_Assign, self, { operator: "=", left: sym, right: value @@ -4203,13 +4205,13 @@ merge(Compressor.prototype, { function flatten_args(decls, expressions) { var len = fn.argnames.length; for (var i = self.args.length; --i >= len;) { - expressions.unshift(self.args[i]); + expressions.push(self.args[i]); } for (i = len; --i >= 0;) { var name = fn.argnames[i]; var value = self.args[i]; if (name.__unused || scope.var_names()[name.name]) { - if (value) expressions.unshift(value); + if (value) expressions.push(value); } else { var symbol = make_node(AST_SymbolVar, name, name); name.definition().orig.push(symbol); @@ -4217,14 +4219,16 @@ merge(Compressor.prototype, { append_var(decls, expressions, symbol, value); } } + decls.reverse(); + expressions.reverse(); } function flatten_vars(decls, expressions) { - if (in_loop) in_loop.length = 0; - for (var i = fn.body.length; --i >= 0;) { + var pos = expressions.length; + for (var i = 0, lines = fn.body.length; i < lines; i++) { var stat = fn.body[i]; if (!(stat instanceof AST_Var)) continue; - for (var j = stat.definitions.length; --j >= 0;) { + for (var j = 0, defs = stat.definitions.length; j < defs; j++) { var var_def = stat.definitions[j]; var name = var_def.name; append_var(decls, expressions, name, var_def.value); @@ -4232,7 +4236,7 @@ merge(Compressor.prototype, { var def = name.definition(); var sym = make_node(AST_SymbolRef, name, name); def.references.push(sym); - in_loop.unshift(make_node(AST_Assign, var_def, { + expressions.splice(pos++, 0, make_node(AST_Assign, var_def, { operator: "=", left: sym, right: make_node(AST_Undefined, name) @@ -4240,14 +4244,13 @@ merge(Compressor.prototype, { } } } - if (in_loop) [].unshift.apply(expressions, in_loop); } function flatten_fn() { var decls = []; var expressions = []; - flatten_vars(decls, expressions); flatten_args(decls, expressions); + flatten_vars(decls, expressions); expressions.push(value); if (decls.length) { i = scope.body.indexOf(compressor.parent(level - 1)) + 1; diff --git a/test/compress/functions.js b/test/compress/functions.js index 5b0c49b8..f4662082 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1887,3 +1887,21 @@ use_before_init_in_loop: { } expect_stdout: "PASS" } + +duplicate_arg_var: { + options = { + inline: true, + toplevel: true, + } + input: { + console.log(function(b) { + return b; + var b; + }("PASS")); + } + expect: { + console.log((b = "PASS", b)); + var b; + } + expect_stdout: "PASS" +} From afbcebddf63c7ffa5b0df9b3712ee3b560918f1e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 5 Jan 2018 05:08:09 +0800 Subject: [PATCH 18/29] fix `mangle` name collision across files (#2722) --- lib/minify.js | 2 -- lib/propmangle.js | 25 +++++++++-------- lib/scope.js | 34 +++++++++++++++-------- test/input/issue-1242/qux.js | 6 ++-- test/mocha/minify.js | 53 ++++++++++++++++++++++++++++++++---- 5 files changed, 87 insertions(+), 33 deletions(-) diff --git a/lib/minify.js b/lib/minify.js index 806c3aeb..a68cbf3a 100644 --- a/lib/minify.js +++ b/lib/minify.js @@ -29,7 +29,6 @@ function set_shorthand(name, options, keys) { function init_cache(cache) { if (!cache) return; - if (!("cname" in cache)) cache.cname = -1; if (!("props" in cache)) { cache.props = new Dictionary(); } else if (!(cache.props instanceof Dictionary)) { @@ -39,7 +38,6 @@ function init_cache(cache) { function to_json(cache) { return { - cname: cache.cname, props: cache.props.toObject() }; } diff --git a/lib/propmangle.js b/lib/propmangle.js index c2f27c42..ffc8faf8 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -110,12 +110,15 @@ function mangle_properties(ast, options) { if (!Array.isArray(reserved)) reserved = []; if (!options.builtins) find_builtins(reserved); - var cache = options.cache; - if (cache == null) { - cache = { - cname: -1, - props: new Dictionary() - }; + var cname = -1; + var cache; + if (options.cache) { + cache = options.cache.props; + cache.each(function(mangled_name) { + push_uniq(reserved, mangled_name); + }); + } else { + cache = new Dictionary(); } var regex = options.regex; @@ -172,7 +175,7 @@ function mangle_properties(ast, options) { if (unmangleable.indexOf(name) >= 0) return false; if (reserved.indexOf(name) >= 0) return false; if (options.only_cache) { - return cache.props.has(name); + return cache.has(name); } if (/^-?[0-9]+(\.[0-9]+)?(e[+-][0-9]+)?$/.test(name)) return false; return true; @@ -181,7 +184,7 @@ function mangle_properties(ast, options) { function should_mangle(name) { if (regex && !regex.test(name)) return false; if (reserved.indexOf(name) >= 0) return false; - return cache.props.has(name) + return cache.has(name) || names_to_mangle.indexOf(name) >= 0; } @@ -199,7 +202,7 @@ function mangle_properties(ast, options) { return name; } - var mangled = cache.props.get(name); + var mangled = cache.get(name); if (!mangled) { if (debug) { // debug mode: use a prefix and suffix to preserve readability, e.g. o.foo -> o._$foo$NNN_. @@ -213,11 +216,11 @@ function mangle_properties(ast, options) { // either debug mode is off, or it is on and we could not use the mangled name if (!mangled) { do { - mangled = base54(++cache.cname); + mangled = base54(++cname); } while (!can_mangle(mangled)); } - cache.props.set(name, mangled); + cache.set(name, mangled); } return mangled; } diff --git a/lib/scope.js b/lib/scope.js index de92fc94..801c888c 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -242,10 +242,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } })); } - - if (options.cache) { - this.cname = options.cache.cname; - } }); AST_Toplevel.DEFMETHOD("def_global", function(node){ @@ -329,10 +325,10 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol, init){ return symbol.thedef = def; }); -AST_Scope.DEFMETHOD("next_mangled", function(options){ - var ext = this.enclosed; +function next_mangled(scope, options) { + var ext = scope.enclosed; out: while (true) { - var m = base54(++this.cname); + var m = base54(++scope.cname); if (!is_identifier(m)) continue; // skip over "do" // https://github.com/mishoo/UglifyJS2/issues/242 -- do not @@ -349,6 +345,18 @@ AST_Scope.DEFMETHOD("next_mangled", function(options){ } return m; } +} + +AST_Scope.DEFMETHOD("next_mangled", function(options){ + return next_mangled(this, options); +}); + +AST_Toplevel.DEFMETHOD("next_mangled", function(options){ + var name; + do { + name = next_mangled(this, options); + } while (member(name, this.mangled_names)); + return name; }); AST_Function.DEFMETHOD("next_mangled", function(options, def){ @@ -362,7 +370,7 @@ AST_Function.DEFMETHOD("next_mangled", function(options, def){ var tricky_name = tricky_def ? tricky_def.mangled_name || tricky_def.name : null; while (true) { - var name = AST_Lambda.prototype.next_mangled.call(this, options, def); + var name = next_mangled(this, options); if (!tricky_name || tricky_name != name) return name; } @@ -413,8 +421,14 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ var lname = -1; var to_mangle = []; + var mangled_names = this.mangled_names = []; if (options.cache) { this.globals.each(collect); + if (options.cache.props) { + options.cache.props.each(function(mangled_name) { + push_uniq(mangled_names, mangled_name); + }); + } } var tw = new TreeWalker(function(node, descend){ @@ -443,10 +457,6 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ this.walk(tw); to_mangle.forEach(function(def){ def.mangle(options) }); - if (options.cache) { - options.cache.cname = this.cname; - } - function collect(symbol) { if (!member(symbol.name, options.reserved)) { to_mangle.push(symbol); diff --git a/test/input/issue-1242/qux.js b/test/input/issue-1242/qux.js index 94171f38..4a72ffc2 100644 --- a/test/input/issue-1242/qux.js +++ b/test/input/issue-1242/qux.js @@ -1,4 +1,4 @@ -var a = bar(1+2); -var b = baz(3+9); -print('q' + 'u' + 'x', a, b); +var x = bar(1+2); +var y = baz(3+9); +print('q' + 'u' + 'x', x, y); foo(5+6); diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 5fa9254b..a304c5b5 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -42,8 +42,15 @@ describe("minify", function() { original += code; 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}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(JSON.stringify(cache).slice(0, 10), '{"props":{'); + assert.strictEqual(compressed, [ + "function n(n){return 3*n}", + "function r(n){return n/2}", + "var o=console.log.bind(console);", + 'function c(n){o("Foo:",2*n)}', + "var a=n(3),b=r(12);", + 'o("qux",a,b),c(11);', + ].join("")); assert.strictEqual(run_code(compressed), run_code(original)); }); @@ -68,12 +75,48 @@ describe("minify", function() { original += code; 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}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(JSON.stringify(cache).slice(0, 18), '{"vars":{"props":{'); + assert.strictEqual(compressed, [ + "function n(n){return 3*n}", + "function r(n){return n/2}", + "var o=console.log.bind(console);", + 'function c(n){o("Foo:",2*n)}', + "var a=n(3),b=r(12);", + 'o("qux",a,b),c(11);', + ].join("")); assert.strictEqual(run_code(compressed), run_code(original)); }); - it("should not parse invalid use of reserved words", function() { + it("Should avoid mangled names in cache", function() { + var cache = {}; + var original = ""; + var compressed = ""; + [ + '"xxxyy";var i={s:1};', + '"xxyyy";var j={t:2,u:3},k=4;', + 'console.log(i.s,j.t,j.u,k);', + ].forEach(function(code) { + var result = Uglify.minify(code, { + compress: false, + mangle: { + properties: true, + toplevel: true + }, + nameCache: cache + }); + if (result.error) throw result.error; + original += code; + compressed += result.code; + }); + assert.strictEqual(compressed, [ + '"xxxyy";var x={x:1};', + '"xxyyy";var y={y:2,a:3},a=4;', + 'console.log(x.x,y.y,y.a,a);', + ].join("")); + assert.strictEqual(run_code(compressed), run_code(original)); + }); + + it("Should not parse invalid use of reserved words", function() { assert.strictEqual(Uglify.minify("function enum(){}").error, undefined); assert.strictEqual(Uglify.minify("function static(){}").error, undefined); assert.strictEqual(Uglify.minify("function this(){}").error.message, "Unexpected token: name (this)"); From 7f2a591c7ecdb13a20bbad5e7c614672dd890431 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 5 Jan 2018 19:36:02 +0800 Subject: [PATCH 19/29] warn on deprecated features (#2726) - `function.arguments` - `function.callers` fixes #2719 --- lib/compress.js | 8 ++++++++ test/compress/issue-2719.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 test/compress/issue-2719.js diff --git a/lib/compress.js b/lib/compress.js index 3a3aac4f..e824d6ff 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -5461,6 +5461,14 @@ merge(Compressor.prototype, { }); OPT(AST_Dot, function(self, compressor){ + if (self.property == "arguments" || self.property == "caller") { + compressor.warn("Function.protoype.{prop} not supported [{file}:{line},{col}]", { + prop: self.property, + file: self.start.file, + line: self.start.line, + col: self.start.col + }); + } var def = self.resolve_defines(compressor); if (def) { return def.optimize(compressor); diff --git a/test/compress/issue-2719.js b/test/compress/issue-2719.js new file mode 100644 index 00000000..93e51d7c --- /dev/null +++ b/test/compress/issue-2719.js @@ -0,0 +1,32 @@ +warn: { + options = { + evaluate: true, + inline: true, + passes: 2, + properties: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + return g(); + } + function g() { + return g["call" + "er"].arguments; + } + // 3 + console.log(f(1, 2, 3).length); + } + expect: { + // TypeError: Cannot read property 'arguments' of null + console.log(function g() { + return g.caller.arguments; + }().length); + } + expect_warnings: [ + "WARN: Function.protoype.caller not supported [test/compress/issue-2719.js:17,19]", + "WARN: Function.protoype.arguments not supported [test/compress/issue-2719.js:17,19]", + ] +} From b82feb9302adddb1763e9915b0daf9ff35ab2af2 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 5 Jan 2018 20:24:30 +0800 Subject: [PATCH 20/29] improve `if_return` (#2727) --- lib/compress.js | 5 +++-- test/compress/if_return.js | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index e824d6ff..7f33b53b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1436,8 +1436,9 @@ merge(Compressor.prototype, { } //--- // if (foo()) return x; [ return ; ] ==> return foo() ? x : undefined; - if (multiple_if_returns && in_lambda && value && !stat.alternative - && (!next || next instanceof AST_Return)) { + if (value && !stat.alternative + && (!next && in_lambda && multiple_if_returns + || next instanceof AST_Return)) { CHANGED = true; stat = stat.clone(); stat.alternative = next || make_node(AST_Return, stat, { diff --git a/test/compress/if_return.js b/test/compress/if_return.js index a0dfdc9a..981b437f 100644 --- a/test/compress/if_return.js +++ b/test/compress/if_return.js @@ -372,3 +372,27 @@ if_var_return: { } } } + +if_if_return_return: { + options = { + conditionals: true, + if_return: true, + } + input: { + function f(a, b) { + if (a) { + if (b) + return b; + return; + } + g(); + } + } + expect: { + function f(a, b) { + if (a) + return b || void 0; + g(); + } + } +} From 9f23185f2bb0d23d5dd2e2334c589e97132a37a8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 5 Jan 2018 22:21:18 +0800 Subject: [PATCH 21/29] fix corner case with `arguments` as function name (#2729) fixes #2728 --- lib/scope.js | 2 +- test/compress/typeof.js | 123 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/lib/scope.js b/lib/scope.js index 801c888c..a18e92a3 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -151,7 +151,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.references = []; } if (node instanceof AST_SymbolLambda) { - defun.def_function(node, defun); + defun.def_function(node, node.name == "arguments" ? undefined : defun); } else if (node instanceof AST_SymbolDefun) { // Careful here, the scope where this should be defined is diff --git a/test/compress/typeof.js b/test/compress/typeof.js index 72e77beb..b5f14016 100644 --- a/test/compress/typeof.js +++ b/test/compress/typeof.js @@ -178,3 +178,126 @@ duplicate_lambda_arg_name: { } expect_stdout: "undefined" } + +issue_2728_1: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + (function arguments() { + console.log(typeof arguments); + })(); + } + expect: { + (function arguments() { + console.log(typeof arguments); + })(); + } + expect_stdout: "object" +} + +issue_2728_2: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + function arguments() { + return typeof arguments; + } + console.log(typeof arguments, arguments()); + } + expect: { + function arguments() { + return typeof arguments; + } + console.log(typeof arguments, arguments()); + } + expect_stdout: "function object" +} + +issue_2728_3: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + (function() { + function arguments() { + } + console.log(typeof arguments); + })(); + } + expect: { + (function() { + function arguments() { + } + console.log("function"); + })(); + } + expect_stdout: "function" +} + +issue_2728_4: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + typeofs: true, + } + input: { + function arguments() { + } + console.log(typeof arguments); + } + expect: { + function arguments() { + } + console.log("function"); + } + expect_stdout: "function" +} + +issue_2728_5: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + (function arguments(arguments) { + console.log(typeof arguments); + })(); + } + expect: { + (function arguments(arguments) { + console.log(typeof arguments); + })(); + } + expect_stdout: "undefined" +} + +issue_2728_6: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + function arguments(arguments) { + return typeof arguments; + } + console.log(typeof arguments, arguments()); + } + expect: { + function arguments(arguments) { + return typeof arguments; + } + console.log(typeof arguments, arguments()); + } + expect_stdout: "function undefined" +} From 9b1bc6c014338422047479365a28b27f1988d353 Mon Sep 17 00:00:00 2001 From: kzc Date: Fri, 5 Jan 2018 16:54:53 -0500 Subject: [PATCH 22/29] ufuzz: add strings "a", "b", "c" to VALUES (#2732) --- test/ufuzz.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/ufuzz.js b/test/ufuzz.js index 1589d5f1..de6c317c 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -127,6 +127,9 @@ for (var i = 2; i < process.argv.length; ++i) { } var VALUES = [ + '"a"', + '"b"', + '"c"', '""', 'true', 'false', From 3505a3604a682138512a7e03964fb68c861b5cbd Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 6 Jan 2018 06:16:51 +0800 Subject: [PATCH 23/29] enhance `unsafe_proto` (#2733) --- lib/compress.js | 16 ++++++++++++++++ test/compress/properties.js | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 7f33b53b..0f15eb7e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -5484,11 +5484,27 @@ merge(Compressor.prototype, { elements: [] }); break; + case "Function": + self.expression = make_node(AST_Function, self.expression, { + argnames: [], + body: [] + }); + break; + case "Number": + self.expression = make_node(AST_Number, self.expression, { + value: 0 + }); + break; case "Object": self.expression = make_node(AST_Object, self.expression, { properties: [] }); break; + case "RegExp": + self.expression = make_node(AST_RegExp, self.expression, { + value: /t/ + }); + break; case "String": self.expression = make_node(AST_String, self.expression, { value: "" diff --git a/test/compress/properties.js b/test/compress/properties.js index 16374ef8..b2ce5f61 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -567,12 +567,18 @@ native_prototype: { } input: { Array.prototype.splice.apply(a, [1, 2, b, c]); + Function.prototype.call.apply(console.log, console, [ "foo" ]); + Number.prototype.toFixed.call(Math.PI, 2); Object.prototype.hasOwnProperty.call(d, "foo"); + RegExp.prototype.test.call(/foo/, "bar"); String.prototype.indexOf.call(e, "bar"); } expect: { [].splice.apply(a, [1, 2, b, c]); + (function() {}).call.apply(console.log, console, [ "foo" ]); + 0..toFixed.call(Math.PI, 2); ({}).hasOwnProperty.call(d, "foo"); + /t/.test.call(/foo/, "bar"); "".indexOf.call(e, "bar"); } } From 3564b4f20dfd2ec06dd162ce5c22b1b3f2e03c7f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 6 Jan 2018 21:04:47 +0800 Subject: [PATCH 24/29] compress `RegExp()` in `unsafe` (#2735) --- lib/compress.js | 21 +++++++++++++++++++++ test/compress/issue-269.js | 24 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 0f15eb7e..28d6a1c4 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3869,6 +3869,27 @@ merge(Compressor.prototype, { operator: "!" }).optimize(compressor); break; + case "RegExp": + var params = []; + if (all(self.args, function(arg) { + var value = arg.evaluate(compressor); + params.unshift(value); + return arg !== value; + })) { + try { + return best_of(compressor, self, make_node(AST_RegExp, self, { + value: RegExp.apply(RegExp, params), + })); + } catch (ex) { + compressor.warn("Error converting {expr} [{file}:{line},{col}]", { + expr: self.print_to_string(), + file: self.start.file, + line: self.start.line, + col: self.start.col + }); + } + } + break; } else if (exp instanceof AST_Dot) switch(exp.property) { case "toString": if (self.args.length == 0) return make_node(AST_Binary, self, { diff --git a/test/compress/issue-269.js b/test/compress/issue-269.js index 1d41dea6..a29e7541 100644 --- a/test/compress/issue-269.js +++ b/test/compress/issue-269.js @@ -64,3 +64,27 @@ strings_concat: { ); } } + +regexp: { + options = { + evaluate: true, + unsafe: true, + } + input: { + RegExp("foo"); + RegExp("bar", "ig"); + RegExp(foo); + RegExp("bar", ig); + RegExp("should", "fail"); + } + expect: { + /foo/; + /bar/ig; + RegExp(foo); + RegExp("bar", ig); + RegExp("should", "fail"); + } + expect_warnings: [ + 'WARN: Error converting RegExp("should","fail") [test/compress/issue-269.js:78,2]', + ] +} From 659c8a763265dfb6d567f5a41422f5f9095af4d8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 6 Jan 2018 21:05:21 +0800 Subject: [PATCH 25/29] handle trailing line comments correctly (#2736) fixes #2734 --- lib/output.js | 8 ++++++-- test/mocha/comment.js | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/output.js b/lib/output.js index 58521e3c..83cd37b8 100644 --- a/lib/output.js +++ b/lib/output.js @@ -534,7 +534,11 @@ function OutputStream(options) { var token = node.end; if (!token) return; var comments = token[tail ? "comments_before" : "comments_after"]; - if (comments && comments._dumped !== self) { + if (comments + && comments._dumped !== self + && (node instanceof AST_Statement || all(comments, function(c) { + return !/comment[134]/.test(c.type); + }))) { comments._dumped = self; var insert = OUTPUT.length; comments.filter(comment_filter, node).forEach(function(c, i) { @@ -599,7 +603,7 @@ function OutputStream(options) { add_mapping : add_mapping, option : function(opt) { return options[opt] }, prepend_comments: readonly ? noop : prepend_comments, - append_comments : readonly ? noop : append_comments, + append_comments : readonly || comment_filter === return_false ? noop : append_comments, line : function() { return current_line }, col : function() { return current_col }, pos : function() { return current_pos }, diff --git a/test/mocha/comment.js b/test/mocha/comment.js index 3bad9e01..4db46b52 100644 --- a/test/mocha/comment.js +++ b/test/mocha/comment.js @@ -219,4 +219,24 @@ describe("Comment", function() { if (result.error) throw result.error; assert.strictEqual(result.code, "/*a*/ /*b*/(function(){/*c*/}/*d*/ /*e*/)();"); }); + + it("Should output line comments after statements", function() { + var result = uglify.minify([ + "x()//foo", + "{y()//bar", + "}", + ].join("\n"), { + compress: false, + mangle: false, + output: { + comments: "all", + }, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code, [ + "x();//foo", + "{y();//bar", + "}", + ].join("\n")); + }); }); From 8430c2f9f85e7217e87cad6756a2d8a083b137bc Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 7 Jan 2018 14:14:58 +0800 Subject: [PATCH 26/29] enable AppVeyor CI (#2739) --- appveyor.yml | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4d8c2e2b..8972925c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,24 +1,20 @@ environment: matrix: - - nodejs_version: "0.10" - - nodejs_version: "0.12" - - nodejs_version: "4.0" - - nodejs_version: "6.0" - + - nodejs_version: "0.10" + - nodejs_version: "0.12" + - nodejs_version: "4" + - nodejs_version: "6" + - nodejs_version: "8" +install: + - ps: Install-Product node $env:nodejs_version + - set UGLIFYJS_TEST_ALL=1 + - npm install +build: off +cache: + - tmp matrix: fast_finish: true - -platform: - - x86 - - x64 - -install: - - ps: Install-Product node $env:nodejs_version $env:platform - - npm install - test_script: - node --version - npm --version - npm test - -build: off From 1ee8be8d91dd94a983e853b7abcc64d6dbc6214b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 7 Jan 2018 15:31:24 +0800 Subject: [PATCH 27/29] fix recursive function `inline` (#2738) fixes #2737 --- lib/compress.js | 5 +++-- test/compress/functions.js | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 28d6a1c4..011bb1b4 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4075,9 +4075,10 @@ merge(Compressor.prototype, { if (compressor.option("inline") && !fn.uses_arguments && !fn.uses_eval + && !(fn.name && fn instanceof AST_Function) && (value = can_flatten_body(stat)) - && (exp === fn ? !fn.name - : compressor.option("unused") + && (exp === fn + || compressor.option("unused") && (def = exp.definition()).references.length == 1 && !recursive_ref(compressor, def) && fn.is_constant_expression(exp.scope)) diff --git a/test/compress/functions.js b/test/compress/functions.js index f4662082..222aa0ca 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -1905,3 +1905,49 @@ duplicate_arg_var: { } expect_stdout: "PASS" } + +issue_2737_1: { + options = { + inline: true, + reduce_vars: true, + unused: true, + } + input: { + (function(a) { + while (a()); + })(function f() { + console.log(typeof f); + }); + } + expect: { + (function(a) { + while (a()); + })(function f() { + console.log(typeof f); + }); + } + expect_stdout: "function" +} + +issue_2737_2: { + options = { + inline: true, + reduce_vars: true, + unused: true, + } + input: { + (function(bar) { + for (;bar(); ) break; + })(function qux() { + return console.log("PASS"), qux; + }); + } + expect: { + (function(bar) { + for (;bar(); ) break; + })(function qux() { + return console.log("PASS"), qux; + }); + } + expect_stdout: "PASS" +} From 9809567dfc4d5c3f23c7e6424da9a471a964584a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 7 Jan 2018 17:53:50 +0800 Subject: [PATCH 28/29] improve `process.exit()` workaround (#2741) - use public API - fix issue with Node.js 0.10 on WIndows --- bin/uglifyjs | 6 +----- test/jetstream.js | 6 +----- test/ufuzz.js | 6 +----- tools/exit.js | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 tools/exit.js diff --git a/bin/uglifyjs b/bin/uglifyjs index 718397c1..38917137 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -3,11 +3,7 @@ "use strict"; -// workaround for tty output truncation upon process.exit() -[process.stdout, process.stderr].forEach(function(stream){ - if (stream._handle && stream._handle.setBlocking) - stream._handle.setBlocking(true); -}); +require("../tools/exit"); var fs = require("fs"); var info = require("../package.json"); diff --git a/test/jetstream.js b/test/jetstream.js index 1cc6d4a7..7ee09df3 100644 --- a/test/jetstream.js +++ b/test/jetstream.js @@ -5,11 +5,7 @@ var site = "http://browserbench.org/JetStream"; if (typeof phantom == "undefined") { - // workaround for tty output truncation upon process.exit() - [process.stdout, process.stderr].forEach(function(stream){ - if (stream._handle && stream._handle.setBlocking) - stream._handle.setBlocking(true); - }); + require("../tools/exit"); var args = process.argv.slice(2); var debug = args.indexOf("--debug"); if (debug >= 0) { diff --git a/test/ufuzz.js b/test/ufuzz.js index de6c317c..d02e9f76 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -6,11 +6,7 @@ // bin/uglifyjs s.js -c && bin/uglifyjs s.js -c passes=3 && bin/uglifyjs s.js -c passes=3 -m // cat s.js | node && node s.js && bin/uglifyjs s.js -c | node && bin/uglifyjs s.js -c passes=3 | node && bin/uglifyjs s.js -c passes=3 -m | node -// workaround for tty output truncation upon process.exit() -[process.stdout, process.stderr].forEach(function(stream){ - if (stream._handle && stream._handle.setBlocking) - stream._handle.setBlocking(true); -}); +require("../tools/exit"); var UglifyJS = require(".."); var randomBytes = require("crypto").randomBytes; diff --git a/tools/exit.js b/tools/exit.js new file mode 100644 index 00000000..17048d8e --- /dev/null +++ b/tools/exit.js @@ -0,0 +1,15 @@ +// workaround for tty output truncation upon process.exit() +var exit = process.exit; +process.exit = function() { + var args = [].slice.call(arguments); + process.once("uncaughtException", function() { + (function callback() { + if (process.stdout.bufferSize || process.stderr.bufferSize) { + setImmediate(callback); + } else { + exit.apply(process, args); + } + })(); + }); + throw exit; +}; From 9336cc824739f6e594e7c020a49b2d17b1faaec2 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 7 Jan 2018 11:51:36 +0000 Subject: [PATCH 29/29] v3.3.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 74878c08..ead43ef5 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "3.3.4", + "version": "3.3.5", "engines": { "node": ">=0.8.0" },