From 011123223b8e45ab3f6d151ad038a6b8ecec2434 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 22 Oct 2017 03:23:31 +0800 Subject: [PATCH 01/16] fix `unsafe` escape analysis in `reduce_vars` (#2387) --- lib/compress.js | 20 +++++++---- test/compress/arrays.js | 69 +++++++++++++++++++----------------- test/compress/functions.js | 12 +++---- test/compress/reduce_vars.js | 29 +++++++++++++++ 4 files changed, 85 insertions(+), 45 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 1f58b390..c7e08638 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -327,13 +327,7 @@ merge(Compressor.prototype, { d.fixed = false; } } else { - var parent = tw.parent(); - if (parent instanceof AST_Assign && parent.operator == "=" && node === parent.right - || parent instanceof AST_Call && node !== parent.expression - || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope - || parent instanceof AST_VarDef && node === parent.value) { - d.escaped = true; - } + mark_escaped(d, node, 0); } } } @@ -579,6 +573,18 @@ merge(Compressor.prototype, { return !immutable && is_modified(parent, level + 1); } } + + function mark_escaped(d, node, level) { + var parent = tw.parent(level); + if (parent instanceof AST_Assign && parent.operator == "=" && node === parent.right + || parent instanceof AST_Call && node !== parent.expression + || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope + || parent instanceof AST_VarDef && node === parent.value) { + d.escaped = true; + } else if (parent instanceof AST_PropAccess && node === parent.expression) { + mark_escaped(d, parent, level + 1); + } + } }); AST_SymbolRef.DEFMETHOD("fixed_value", function() { diff --git a/test/compress/arrays.js b/test/compress/arrays.js index f0ded06c..539dfb04 100644 --- a/test/compress/arrays.js +++ b/test/compress/arrays.js @@ -128,50 +128,55 @@ constant_join_3: { for_loop: { options = { - unsafe : true, - unused : true, - evaluate : true, - reduce_vars : true + evaluate: true, + reduce_vars: true, + unsafe: true, }; input: { function f0() { var a = [1, 2, 3]; - for (var i = 0; i < a.length; i++) { - console.log(a[i]); - } + var b = 0; + for (var i = 0; i < a.length; i++) + b += a[i]; + return b; } - function f1() { var a = [1, 2, 3]; - for (var i = 0, len = a.length; i < len; i++) { - console.log(a[i]); - } + var b = 0; + for (var i = 0, len = a.length; i < len; i++) + b += a[i]; + return b; } - - function f2() { - var a = [1, 2, 3]; - for (var i = 0; i < a.length; i++) { - a[i]++; - } - } - } - expect: { - function f0() { - var a = [1, 2, 3]; - for (var i = 0; i < 3; i++) - console.log(a[i]); - } - - function f1() { - var a = [1, 2, 3]; - for (var i = 0; i < 3; i++) - console.log(a[i]); - } - function f2() { var a = [1, 2, 3]; for (var i = 0; i < a.length; i++) a[i]++; + return a[2]; } + console.log(f0(), f1(), f2()); } + expect: { + function f0() { + var a = [1, 2, 3]; + var b = 0; + for (var i = 0; i < 3; i++) + b += a[i]; + return b; + } + function f1() { + var a = [1, 2, 3]; + var b = 0; + for (var i = 0, len = a.length; i < len; i++) + b += a[i]; + return b; + } + function f2() { + var a = [1, 2, 3]; + for (var i = 0; i < a.length; i++) + a[i]++; + return a[2]; + } + console.log(f0(), f1(), f2()); + } + expect_stdout: "6 6 4" } diff --git a/test/compress/functions.js b/test/compress/functions.js index f411afa2..6c82557d 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -151,13 +151,13 @@ issue_1841_2: { function_returning_constant_literal: { options = { - reduce_vars: true, - unsafe: true, - toplevel: true, - evaluate: true, - cascade: true, - unused: true, inline: true, + passes: 2, + reduce_vars: true, + side_effects: true, + toplevel: true, + unsafe: true, + unused: true, } input: { function greeter() { diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index a03bc1c8..681dafd3 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2954,3 +2954,32 @@ const_expr_2: { } expect_stdout: "2 2" } + +escaped_prop: { + options = { + collapse_vars: true, + evaluate: true, + inline: true, + pure_getters: "strict", + reduce_vars: true, + side_effects: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + var obj = { o: { a: 1 } }; + (function(o) { + o.a++; + })(obj.o); + (function(o) { + console.log(o.a); + })(obj.o); + } + expect: { + var obj = { o: { a: 1 } }; + obj.o.a++; + console.log(obj.o.a); + } + expect_stdout: "2" +} From 4ae1fb3ed8380d06220fd242037bd277fc21c17d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 22 Oct 2017 04:19:40 +0800 Subject: [PATCH 02/16] fix `unsafe` evaluation of objects (#2388) --- lib/compress.js | 1 + test/compress/properties.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index c7e08638..cef5d75f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4492,6 +4492,7 @@ merge(Compressor.prototype, { if (def) { return def.optimize(compressor); } + if (is_lhs(self, compressor.parent())) return self; if (compressor.option("unsafe") && self.expression instanceof AST_Object) { var values = self.expression.properties; for (var i = values.length; --i >= 0;) { diff --git a/test/compress/properties.js b/test/compress/properties.js index c2c43f69..c8a85697 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -804,3 +804,21 @@ issue_2256: { g.keep = g.g; } } + +lhs_prop: { + options = { + evaluate: true, + unsafe: true, + } + input: { + console.log(++{ + a: 1 + }.a); + } + expect: { + console.log(++{ + a: 1 + }.a); + } + expect_stdout: "2" +} From 516eaef50c66aede72d3478a5653ae8651806258 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 22 Oct 2017 13:14:15 +0800 Subject: [PATCH 03/16] fix `unsafe` evaluation of `AST_Sub` (#2389) --- lib/compress.js | 1 + test/compress/properties.js | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index cef5d75f..6a40ef2a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4468,6 +4468,7 @@ merge(Compressor.prototype, { }); } } + if (is_lhs(self, compressor.parent())) return self; var ev = self.evaluate(compressor); if (ev !== self) { ev = make_node_from_constant(ev, self).optimize(compressor); diff --git a/test/compress/properties.js b/test/compress/properties.js index c8a85697..45f870df 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -805,7 +805,7 @@ issue_2256: { } } -lhs_prop: { +lhs_prop_1: { options = { evaluate: true, unsafe: true, @@ -822,3 +822,22 @@ lhs_prop: { } expect_stdout: "2" } + +lhs_prop_2: { + options = { + evaluate: true, + inline: true, + reduce_vars: true, + side_effects: true, + unsafe: true, + unused: true, + } + input: { + (function(a) { + a[2] = "g"; + })("abc"); + } + expect: { + "abc"[2] = "g"; + } +} From 5fd723f14394b74f899e7b33bc9084317bf01d7d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 22 Oct 2017 15:00:36 +0800 Subject: [PATCH 04/16] fix `unsafe` expansion of object literals (#2390) --- lib/compress.js | 11 +++++------ test/compress/properties.js | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 6a40ef2a..8bc0e262 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4499,12 +4499,11 @@ merge(Compressor.prototype, { for (var i = values.length; --i >= 0;) { if (values[i].key === self.property) { var value = values[i].value; - if (value instanceof AST_Function ? !value.contains_this() : !value.has_side_effects(compressor)) { - var obj = self.expression.clone(); - obj.properties = obj.properties.slice(); - obj.properties.splice(i, 1); - return make_sequence(self, [ obj, value ]).optimize(compressor); - } + if (value instanceof AST_Function ? value.contains_this() : value.has_side_effects(compressor)) break; + var obj = self.expression.clone(); + obj.properties = obj.properties.slice(); + obj.properties.splice(i, 1); + return make_sequence(self, [ obj, value ]).optimize(compressor); } } } diff --git a/test/compress/properties.js b/test/compress/properties.js index 45f870df..496a43ca 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -841,3 +841,22 @@ lhs_prop_2: { "abc"[2] = "g"; } } + +literal_duplicate_key_side_effects: { + options = { + unsafe: true, + } + input: { + console.log({ + a: "FAIL", + a: console.log ? "PASS" : "FAIL" + }.a); + } + expect: { + console.log({ + a: "FAIL", + a: console.log ? "PASS" : "FAIL" + }.a); + } + expect_stdout: "PASS" +} From 24aa07855bc608f29cca2a58a40af1988256b116 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 22 Oct 2017 20:10:13 +0800 Subject: [PATCH 05/16] safer `properties` transform (#2391) `{ a: x, b: y }.a` => `[ x, y ][0]` - `x` cannot be function containing `this` `[ x, y, z ][1]` => `(x, z, y)` - only if `z` is side-effect-free --- lib/compress.js | 100 ++++++++++++++++------ test/compress/collapse_vars.js | 67 --------------- test/compress/evaluate.js | 22 ++--- test/compress/functions.js | 2 +- test/compress/properties.js | 151 ++++++++++++++++++++++++++++++--- test/compress/reduce_vars.js | 4 +- 6 files changed, 226 insertions(+), 120 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 8bc0e262..eb0e2016 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4452,20 +4452,56 @@ merge(Compressor.prototype, { }); OPT(AST_Sub, function(self, compressor){ - var prop = self.property; - if (prop instanceof AST_String && compressor.option("properties")) { - prop = prop.getValue(); - if (is_identifier_string(prop)) { - return make_node(AST_Dot, self, { - expression : self.expression, - property : prop - }).optimize(compressor); + if (compressor.option("properties")) { + var prop = self.property; + if (prop instanceof AST_String) { + prop = prop.getValue(); + if (is_identifier_string(prop)) { + return make_node(AST_Dot, self, { + expression : self.expression, + property : prop + }).optimize(compressor); + } + var v = parseFloat(prop); + if (!isNaN(v) && v.toString() == prop) { + self.property = make_node(AST_Number, self.property, { + value: v + }); + } } - var v = parseFloat(prop); - if (!isNaN(v) && v.toString() == prop) { - self.property = make_node(AST_Number, self.property, { - value: v - }); + if (prop instanceof AST_Number && self.expression instanceof AST_Array) { + prop = prop.getValue(); + var elements = self.expression.elements; + if (prop in elements) { + var flatten = true; + var values = []; + for (var i = elements.length; --i > prop;) { + var value = elements[i].drop_side_effect_free(compressor); + if (value) { + values.unshift(value); + if (flatten && value.has_side_effects(compressor)) flatten = false; + } + } + var retValue = elements[prop]; + retValue = retValue instanceof AST_Hole ? make_node(AST_Undefined, retValue) : retValue; + if (!flatten) values.unshift(retValue); + while (--i >= 0) { + var value = elements[i].drop_side_effect_free(compressor); + if (value) values.unshift(value); + else prop--; + } + if (flatten) { + values.push(retValue); + return make_sequence(self, values).optimize(compressor); + } else return make_node(AST_Sub, self, { + expression: make_node(AST_Array, self.expression, { + elements: values + }), + property: make_node(AST_Number, self.property, { + value: prop + }) + }); + } } } if (is_lhs(self, compressor.parent())) return self; @@ -4493,20 +4529,6 @@ merge(Compressor.prototype, { if (def) { return def.optimize(compressor); } - if (is_lhs(self, compressor.parent())) return self; - if (compressor.option("unsafe") && self.expression instanceof AST_Object) { - var values = self.expression.properties; - for (var i = values.length; --i >= 0;) { - if (values[i].key === self.property) { - var value = values[i].value; - if (value instanceof AST_Function ? value.contains_this() : value.has_side_effects(compressor)) break; - var obj = self.expression.clone(); - obj.properties = obj.properties.slice(); - obj.properties.splice(i, 1); - return make_sequence(self, [ obj, value ]).optimize(compressor); - } - } - } if (compressor.option("unsafe_proto") && self.expression instanceof AST_Dot && self.expression.property == "prototype") { @@ -4529,6 +4551,30 @@ merge(Compressor.prototype, { break; } } + if (is_lhs(self, compressor.parent())) return self; + if (compressor.option("properties") && self.expression instanceof AST_Object) { + var props = self.expression.properties; + for (var i = props.length; --i >= 0;) { + var prop = props[i]; + if (prop.key === self.property) { + if (!all(props, function(prop) { + return prop instanceof AST_ObjectKeyVal; + })) break; + var value = prop.value; + if (value instanceof AST_Function && value.contains_this()) break; + return make_node(AST_Sub, self, { + expression: make_node(AST_Array, self.expression, { + elements: props.map(function(prop) { + return prop.value; + }) + }), + property: make_node(AST_Number, self, { + value: i + }) + }).optimize(compressor); + } + } + } var ev = self.evaluate(compressor); if (ev !== self) { ev = make_node_from_constant(ev, self).optimize(compressor); diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 52b2ddf8..1f702ad7 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -2534,73 +2534,6 @@ issue_2319_3: { expect_stdout: "true" } -prop_side_effects_1: { - options = { - collapse_vars: true, - evaluate: true, - pure_getters: "strict", - reduce_vars: true, - toplevel: true, - unsafe: true, - unused: true, - } - input: { - var C = 1; - console.log(C); - var obj = { - bar: function() { - return C + C; - } - }; - console.log(obj.bar()); - } - expect: { - console.log(1); - console.log({ - bar: function() { - return 2; - } - }.bar()); - } - expect_stdout: [ - "1", - "2", - ] -} - -prop_side_effects_2: { - options = { - collapse_vars: true, - evaluate: true, - inline: true, - passes: 2, - pure_getters: "strict", - reduce_vars: true, - side_effects: true, - toplevel: true, - unsafe: true, - unused: true, - } - input: { - var C = 1; - console.log(C); - var obj = { - bar: function() { - return C + C; - } - }; - console.log(obj.bar()); - } - expect: { - console.log(1); - console.log(2); - } - expect_stdout: [ - "1", - "2", - ] -} - issue_2365: { options = { collapse_vars: true, diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 5f5a4a93..fe9464bc 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -386,10 +386,10 @@ unsafe_object_accessor: { } } -unsafe_function: { +prop_function: { options = { - evaluate : true, - unsafe : true + evaluate: true, + properties: true, } input: { console.log( @@ -402,9 +402,9 @@ unsafe_function: { expect: { console.log( ({a:{b:1},b:function(){}}) + 1, - ({b:function(){}}, {b:1}) + 1, - ({a:{b:1}}, function(){}) + 1, - ({b:function(){}}, {b:1}).b + 1 + ({b:1}) + 1, + function(){} + 1, + 2 ); } expect_stdout: true @@ -630,10 +630,10 @@ unsafe_string_bad_index: { expect_stdout: true } -unsafe_prototype_function: { +prototype_function: { options = { - evaluate : true, - unsafe : true + evaluate: true, + properties: true, } input: { var a = ({valueOf: 0}) < 1; @@ -652,8 +652,8 @@ unsafe_prototype_function: { var d = ({toString: 0}) + ""; var e = (({valueOf: 0}) + "")[2]; var f = (({toString: 0}) + "")[2]; - var g = ({}, 0)(); - var h = ({}, 0)(); + var g = 0(); + var h = 0(); } } diff --git a/test/compress/functions.js b/test/compress/functions.js index 6c82557d..febf81c1 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -153,10 +153,10 @@ function_returning_constant_literal: { options = { inline: true, passes: 2, + properties: true, reduce_vars: true, side_effects: true, toplevel: true, - unsafe: true, unused: true, } input: { diff --git a/test/compress/properties.js b/test/compress/properties.js index 496a43ca..f435d371 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -677,8 +677,8 @@ accessor_this: { issue_2208_1: { options = { inline: true, + properties: true, side_effects: true, - unsafe: true, } input: { console.log({ @@ -696,8 +696,8 @@ issue_2208_1: { issue_2208_2: { options = { inline: true, + properties: true, side_effects: true, - unsafe: true, } input: { console.log({ @@ -721,8 +721,8 @@ issue_2208_2: { issue_2208_3: { options = { inline: true, + properties: true, side_effects: true, - unsafe: true, } input: { a = 42; @@ -746,8 +746,8 @@ issue_2208_3: { issue_2208_4: { options = { inline: true, + properties: true, side_effects: true, - unsafe: true, } input: { function foo() {} @@ -770,8 +770,8 @@ issue_2208_4: { issue_2208_5: { options = { inline: true, + properties: true, side_effects: true, - unsafe: true, } input: { console.log({ @@ -808,7 +808,7 @@ issue_2256: { lhs_prop_1: { options = { evaluate: true, - unsafe: true, + properties: true, } input: { console.log(++{ @@ -827,9 +827,9 @@ lhs_prop_2: { options = { evaluate: true, inline: true, + properties: true, reduce_vars: true, side_effects: true, - unsafe: true, unused: true, } input: { @@ -844,7 +844,7 @@ lhs_prop_2: { literal_duplicate_key_side_effects: { options = { - unsafe: true, + properties: true, } input: { console.log({ @@ -853,10 +853,137 @@ literal_duplicate_key_side_effects: { }.a); } expect: { - console.log({ - a: "FAIL", - a: console.log ? "PASS" : "FAIL" - }.a); + console.log(console.log ? "PASS" : "FAIL"); } expect_stdout: "PASS" } + +prop_side_effects_1: { + options = { + evaluate: true, + inline: true, + properties: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var C = 1; + console.log(C); + var obj = { + bar: function() { + return C + C; + } + }; + console.log(obj.bar()); + } + expect: { + console.log(1); + var obj = { + bar: function() { + return 2; + } + }; + console.log(obj.bar()); + } + expect_stdout: [ + "1", + "2", + ] +} + +prop_side_effects_2: { + options = { + evaluate: true, + inline: true, + passes: 2, + properties: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var C = 1; + console.log(C); + var obj = { + bar: function() { + return C + C; + } + }; + console.log(obj.bar()); + } + expect: { + console.log(1); + console.log(2); + } + expect_stdout: [ + "1", + "2", + ] +} + +accessor_1: { + options = { + properties: true, + } + input: { + console.log({ + a: "FAIL", + get a() { + return "PASS"; + } + }.a); + } + expect: { + console.log({ + a: "FAIL", + get a() { + return "PASS"; + } + }.a); + } + expect_stdout: "PASS" + node_version: ">=4" +} + +accessor_2: { + options = { + properties: true, + } + input: { + console.log({ + get a() { + return "PASS"; + }, + set a(v) {}, + a: "FAIL" + }.a); + } + expect: { + console.log({ + get a() { + return "PASS"; + }, + set a(v) {}, + a: "FAIL" + }.a); + } + expect_stdout: true +} + +array_hole: { + options = { + properties: true, + } + input: { + console.log( + [ 1, 2, , 3][1], + [ 1, 2, , 3][2], + [ 1, 2, , 3][3] + ); + } + expect: { + console.log(2, void 0, 3); + } + expect_stdout: "2 undefined 3" +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 681dafd3..1274024f 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2660,8 +2660,8 @@ obj_var_2: { evaluate: true, inline: true, passes: 2, + properties: true, reduce_vars: true, - side_effects: true, toplevel: true, unsafe: true, unused: true, @@ -2716,10 +2716,10 @@ obj_arg_2: { evaluate: true, inline: true, passes: 2, + properties: true, reduce_vars: true, side_effects: true, toplevel: true, - unsafe: true, unused: true, } input: { From 8a713e449f551d89745d9a8105b7cb33e9893214 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 23 Oct 2017 01:00:50 +0800 Subject: [PATCH 06/16] deduplicate declarations regardless of `toplevel` (#2393) --- lib/compress.js | 8 ++++---- test/compress/collapse_vars.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index eb0e2016..7085fcb3 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2240,7 +2240,6 @@ merge(Compressor.prototype, { if (self.uses_eval || self.uses_with) return; var drop_funcs = !(self instanceof AST_Toplevel) || compressor.toplevel.funcs; var drop_vars = !(self instanceof AST_Toplevel) || compressor.toplevel.vars; - if (!drop_funcs && !drop_vars) return; var assign_as_unused = /keep_assign/.test(compressor.option("unused")) ? return_false : function(node) { if (node instanceof AST_Assign && (node.write_only || node.operator == "=")) { return node.left; @@ -2375,7 +2374,8 @@ merge(Compressor.prototype, { } return node; } - if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn && tt.parent().init === node)) { + var parent = tt.parent(); + if (node instanceof AST_Definitions && !(parent instanceof AST_ForIn && parent.init === node)) { // place uninitialized names at the start var body = [], head = [], tail = []; // for unused names whose initialization has @@ -2385,7 +2385,7 @@ merge(Compressor.prototype, { node.definitions.forEach(function(def) { if (def.value) def.value = def.value.transform(tt); var sym = def.name.definition(); - if (sym.id in in_use_ids) { + if (!drop_vars || sym.id in in_use_ids) { if (def.name instanceof AST_SymbolVar) { var var_defs = var_defs_by_id.get(sym.id); if (var_defs.length > 1 && !def.value) { @@ -2467,7 +2467,7 @@ merge(Compressor.prototype, { && !((def = def.definition()).id in in_use_ids) && self.variables.get(def.name) === def) { if (node instanceof AST_Assign) { - return maintain_this_binding(tt.parent(), node, node.right.transform(tt)); + return maintain_this_binding(parent, node, node.right.transform(tt)); } return make_node(AST_Number, node, { value: 0 diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 1f702ad7..b5b97d24 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -2051,7 +2051,7 @@ inner_lvalues: { console.log(null, a, b); } expect: { - var a, b = 10; + var b = 10; var a = (--b || a || 3).toString(), c = --b + -a; console.log(null, a, b); } From 86ea38a25954ba4cda017fbfee5e0ef537b24bf1 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 24 Oct 2017 02:58:30 +0800 Subject: [PATCH 07/16] enhance `unsafe` `evaluate` of arrays & objects (#2394) --- lib/compress.js | 47 ++++-- test/compress/reduce_vars.js | 290 ++++++++++++++++++++++++++++++++++- 2 files changed, 323 insertions(+), 14 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 7085fcb3..25718a5e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -311,7 +311,7 @@ merge(Compressor.prototype, { d.references.push(node); if (d.fixed === undefined || !safe_to_read(d) || d.single_use == "m") { d.fixed = false; - } else { + } else if (d.fixed) { var value = node.fixed_value(); if (unused) { d.single_use = value @@ -320,14 +320,14 @@ merge(Compressor.prototype, { && d.scope === node.scope && value.is_constant_expression(); } - if (is_modified(node, 0, is_immutable(value))) { + if (is_modified(node, value, 0, is_immutable(value))) { if (d.single_use) { d.single_use = "m"; } else { d.fixed = false; } } else { - mark_escaped(d, node, 0); + mark_escaped(d, node, value, 0); } } } @@ -564,25 +564,46 @@ merge(Compressor.prototype, { return value && (value.is_constant() || value instanceof AST_Lambda); } - function is_modified(node, level, immutable) { - var parent = tw.parent(level); - if (is_lhs(node, parent) - || !immutable && parent instanceof AST_Call && parent.expression === node) { - return true; - } else if (parent instanceof AST_PropAccess && parent.expression === node) { - return !immutable && is_modified(parent, level + 1); + function read_property(obj, key) { + if (key instanceof AST_Constant) key = key.getValue(); + if (key instanceof AST_Node) return null; + if (obj instanceof AST_Array) { + return obj.elements[key]; + } else if (obj instanceof AST_Object) { + var props = obj.properties; + var value; + for (var i = props.length; --i >= 0;) { + var prop = props[i]; + if (!(prop instanceof AST_ObjectKeyVal)) return; + if (!value && props[i].key === key) value = props[i].value; + } + return value; } } - function mark_escaped(d, node, level) { + function is_modified(node, value, level, immutable) { var parent = tw.parent(level); + if (is_lhs(node, parent) + || !immutable + && parent instanceof AST_Call + && parent.expression === node + && (!(value instanceof AST_Function) || value.contains_this())) { + return true; + } else if (parent instanceof AST_PropAccess && parent.expression === node) { + return !immutable && is_modified(parent, read_property(value, parent.property), level + 1); + } + } + + function mark_escaped(d, node, value, level) { + var parent = tw.parent(level); + if (value instanceof AST_Constant || value instanceof AST_Function) return; if (parent instanceof AST_Assign && parent.operator == "=" && node === parent.right || parent instanceof AST_Call && node !== parent.expression || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope || parent instanceof AST_VarDef && node === parent.value) { d.escaped = true; } else if (parent instanceof AST_PropAccess && node === parent.expression) { - mark_escaped(d, parent, level + 1); + mark_escaped(d, parent, read_property(value, parent.property), level + 1); } } }); @@ -1660,6 +1681,7 @@ merge(Compressor.prototype, { var elements = []; for (var i = 0, len = this.elements.length; i < len; i++) { var element = this.elements[i]; + if (element instanceof AST_Function) continue; var value = ev(element, compressor); if (element === value) return this; elements.push(value); @@ -1683,6 +1705,7 @@ merge(Compressor.prototype, { if (typeof Object.prototype[key] === 'function') { return this; } + if (prop.value instanceof AST_Function) continue; val[key] = ev(prop.value, compressor); if (val[key] === prop.value) return this; } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 1274024f..f516b9db 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -211,7 +211,91 @@ unsafe_evaluate: { } } -unsafe_evaluate_object: { +unsafe_evaluate_side_effect_free: { + options = { + evaluate: true, + reduce_vars: true, + unsafe: true, + unused: true, + } + input: { + console.log(function(){ var o={p:1}; console.log(o.p); return o.p; }()); + console.log(function(){ var o={p:2}; console.log(o.p); return o; }()); + } + expect: { + console.log(function(){ console.log(1); return 1; }()); + console.log(function(){ var o={p:2}; console.log(2); return o; }()); + } + expect_stdout: true +} + +unsafe_evaluate_escaped: { + options = { + evaluate: true, + reduce_vars: true, + unsafe: true, + unused: true, + } + input: { + console.log(function(){ var o={p:1}; console.log(o, o.p); return o.p; }()); + console.log(function(){ var o={p:2}; console.log(o.p, o); return o.p; }()); + } + expect: { + console.log(function(){ var o={p:1}; console.log(o, o.p); return o.p; }()); + console.log(function(){ var o={p:2}; console.log(o.p, o); return o.p; }()); + } + expect_stdout: true +} + +unsafe_evaluate_modified: { + options = { + evaluate: true, + reduce_vars: true, + unsafe: true, + unused: true, + } + input: { + 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={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; }()); + } + expect: { + 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={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; }()); + } + expect_stdout: true +} + +unsafe_evaluate_unknown: { + options = { + evaluate: true, + reduce_vars: true, + unsafe: true, + unused: true, + } + input: { + console.log(function(){ var o={p:1}; console.log(o.not_present); return o.p; }()); + console.log(function(){ var o={p:2}; console.log(o.prototype); return o.p; }()); + console.log(function(){ var o={p:3}; console.log(o.hasOwnProperty); return o.p; }()); + } + expect: { + console.log(function(){ var o={p:1}; console.log(o.not_present); return o.p; }()); + console.log(function(){ var o={p:2}; console.log(o.prototype); return o.p; }()); + console.log(function(){ var o={p:3}; console.log(o.hasOwnProperty); return o.p; }()); + } + expect_stdout: true +} + +unsafe_evaluate_object_1: { options = { evaluate : true, reduce_vars : true, @@ -251,7 +335,83 @@ unsafe_evaluate_object: { } } -unsafe_evaluate_array: { +unsafe_evaluate_object_2: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + } + input: { + var obj = { + foo: 1, + bar: 2, + square: function(x) { + return x * x; + }, + cube: function(x) { + return x * x * x; + }, + }; + console.log(obj.foo, obj.bar, obj.square(2), obj.cube); + } + expect: { + var obj = { + foo: 1, + bar: 2, + square: function(x) { + return x * x; + }, + cube: function(x) { + return x * x * x; + }, + }; + console.log(1, 2, obj.square(2), obj.cube); + } + expect_stdout: true +} + +unsafe_evaluate_object_3: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + } + input: { + var obj = { + get foo() { + return 1; + }, + bar: 2, + square: function(x) { + return x * x; + }, + cube: function(x) { + return x * x * x; + }, + }; + console.log(obj.foo, obj.bar, obj.square(2), obj.cube); + } + expect: { + var obj = { + get foo() { + return 1; + }, + bar: 2, + square: function(x) { + return x * x; + }, + cube: function(x) { + return x * x * x; + }, + }; + console.log(obj.foo, obj.bar, obj.square(2), obj.cube); + } + expect_stdout: true +} + +unsafe_evaluate_array_1: { options = { evaluate : true, reduce_vars : true, @@ -299,6 +459,132 @@ unsafe_evaluate_array: { } } +unsafe_evaluate_array_2: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + } + input: { + var arr = [ + 1, + 2, + function(x) { + return x * x; + }, + function(x) { + return x * x * x; + }, + ]; + console.log(arr[0], arr[1], arr[2](2), arr[3]); + } + expect: { + var arr = [ + 1, + 2, + function(x) { + return x * x; + }, + function(x) { + return x * x * x; + }, + ]; + console.log(1, 2, arr[2](2), arr[3]); + } + expect_stdout: true +} + +unsafe_evaluate_array_3: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + } + input: { + var arr = [ + 1, + 2, + function() { + return ++arr[0]; + }, + ]; + console.log(arr[0], arr[1], arr[2](), arr[0]); + } + expect: { + var arr = [ + 1, + 2, + function() { + return ++arr[0]; + }, + ]; + console.log(arr[0], arr[1], arr[2](), arr[0]); + } + expect_stdout: "1 2 2 2" +} + +unsafe_evaluate_array_4: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + } + input: { + var arr = [ + 1, + 2, + function() { + return ++this[0]; + }, + ]; + console.log(arr[0], arr[1], arr[2], arr[0]); + } + expect: { + var arr = [ + 1, + 2, + function() { + return ++this[0]; + }, + ]; + console.log(1, 2, arr[2], 1); + } + expect_stdout: true +} + +unsafe_evaluate_array_5: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + } + input: { + var arr = [ + 1, + 2, + function() { + return ++this[0]; + }, + ]; + console.log(arr[0], arr[1], arr[2](), arr[0]); + } + expect: { + var arr = [ + 1, + 2, + function() { + return ++this[0]; + }, + ]; + console.log(arr[0], arr[1], arr[2](), arr[0]); + } + expect_stdout: "1 2 2 2" +} + unsafe_evaluate_equality_1: { options = { evaluate : true, From 1968203d83ea6ba9dd34b36c0d6f3e4b1c5db340 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Mon, 23 Oct 2017 13:53:56 -0700 Subject: [PATCH 08/16] docs: Fix spelling and style (#2395) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f31486e8..1c4d6114 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ to prevent the `require`, `exports` and `$` names from being changed. is a separate step, different from variable name mangling. Pass `--mangle-props` to enable it. It will mangle all properties in the input code with the exception of built in DOM properties and properties -in core javascript classes. For example: +in core JavaScript classes. For example: ```javascript // example.js @@ -236,7 +236,7 @@ x.bar_ = 2; x["baz_"] = 3; console.log(x.calc()); ``` -Mangle all properties (except for javascript `builtins`): +Mangle all properties (except for JavaScript `builtins`): ```bash $ uglifyjs example.js -c -m --mangle-props ``` @@ -1060,7 +1060,7 @@ in total it's a bit more than just using UglifyJS's own parser. ### Uglify Fast Minify Mode It's not well known, but whitespace removal and symbol mangling accounts -for 95% of the size reduction in minified code for most javascript - not +for 95% of the size reduction in minified code for most JavaScript - not elaborate code transforms. One can simply disable `compress` to speed up Uglify builds by 3 to 4 times. In this fast `mangle`-only mode Uglify has comparable minify speeds and gzip sizes to From 74ae16f9f88600c90438e85829a615b2377f6740 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 24 Oct 2017 22:10:36 +0800 Subject: [PATCH 09/16] fix `unsafe` `reduce_vars` on arrays & objects (#2397) --- lib/compress.js | 10 +++++++--- test/compress/reduce_vars.js | 27 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 25718a5e..670a3b0d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -567,18 +567,18 @@ merge(Compressor.prototype, { function read_property(obj, key) { if (key instanceof AST_Constant) key = key.getValue(); if (key instanceof AST_Node) return null; + var value; if (obj instanceof AST_Array) { - return obj.elements[key]; + value = obj.elements[key]; } else if (obj instanceof AST_Object) { var props = obj.properties; - var value; for (var i = props.length; --i >= 0;) { var prop = props[i]; if (!(prop instanceof AST_ObjectKeyVal)) return; if (!value && props[i].key === key) value = props[i].value; } - return value; } + return value instanceof AST_SymbolRef ? value.fixed_value() : value; } function is_modified(node, value, level, immutable) { @@ -589,6 +589,8 @@ merge(Compressor.prototype, { && parent.expression === node && (!(value instanceof AST_Function) || value.contains_this())) { return true; + } else if (parent instanceof AST_Array || parent instanceof AST_Object) { + return is_modified(parent, parent, level + 1); } else if (parent instanceof AST_PropAccess && parent.expression === node) { return !immutable && is_modified(parent, read_property(value, parent.property), level + 1); } @@ -602,6 +604,8 @@ merge(Compressor.prototype, { || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope || parent instanceof AST_VarDef && node === parent.value) { d.escaped = true; + } else if (parent instanceof AST_Array || parent instanceof AST_Object) { + mark_escaped(d, parent, parent, level + 1); } else if (parent instanceof AST_PropAccess && node === parent.expression) { mark_escaped(d, parent, read_property(value, parent.property), level + 1); } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index f516b9db..ac4fa40b 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -211,7 +211,7 @@ unsafe_evaluate: { } } -unsafe_evaluate_side_effect_free: { +unsafe_evaluate_side_effect_free_1: { options = { evaluate: true, reduce_vars: true, @@ -221,10 +221,31 @@ unsafe_evaluate_side_effect_free: { input: { console.log(function(){ var o={p:1}; console.log(o.p); return o.p; }()); console.log(function(){ var o={p:2}; console.log(o.p); return o; }()); + console.log(function(){ var o={p:3}; console.log([o][0].p); return o.p; }()); } expect: { console.log(function(){ console.log(1); return 1; }()); console.log(function(){ var o={p:2}; console.log(2); return o; }()); + console.log(function(){ console.log(3); return 3; }()); + } + expect_stdout: true +} + +unsafe_evaluate_side_effect_free_2: { + options = { + collapse_vars: true, + evaluate: true, + passes: 2, + pure_getters: "strict", + reduce_vars: true, + unsafe: true, + unused: true, + } + input: { + console.log(function(){ var o={p:1},a=[o]; console.log(a[0].p); return o.p; }()); + } + expect: { + console.log(function(){ console.log(1); return 1; }()); } expect_stdout: true } @@ -239,10 +260,12 @@ unsafe_evaluate_escaped: { input: { console.log(function(){ var o={p:1}; console.log(o, o.p); return o.p; }()); console.log(function(){ var o={p:2}; console.log(o.p, o); return o.p; }()); + console.log(function(){ var o={p:3},a=[o]; console.log(a[0].p++); return o.p; }()); } expect: { console.log(function(){ var o={p:1}; console.log(o, o.p); return o.p; }()); console.log(function(){ var o={p:2}; console.log(o.p, o); return o.p; }()); + console.log(function(){ var o={p:3},a=[o]; console.log(a[0].p++); return o.p; }()); } expect_stdout: true } @@ -262,6 +285,7 @@ unsafe_evaluate_modified: { 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; }()); + console.log(function(){ var o={p:7}; console.log([o][0].p++); return o.p; }()); } expect: { console.log(function(){ var o={p:1}; o.p++; console.log(o.p); return o.p; }()); @@ -271,6 +295,7 @@ unsafe_evaluate_modified: { 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; }()); + console.log(function(){ var o={p:7}; console.log([o][0].p++); return o.p; }()); } expect_stdout: true } From 4178289c382caf2eb3464390370dd1400a23468a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 25 Oct 2017 03:38:11 +0800 Subject: [PATCH 10/16] implement `hoist_props` (#2396) fixes #2377 --- lib/compress.js | 82 +++++++- test/compress/hoist_props.js | 371 +++++++++++++++++++++++++++++++++++ test/ufuzz.json | 6 +- 3 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 test/compress/hoist_props.js diff --git a/lib/compress.js b/lib/compress.js index 670a3b0d..9f410718 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -60,6 +60,7 @@ function Compressor(options, false_by_default) { expression : false, global_defs : {}, hoist_funs : !false_by_default, + hoist_props : false, hoist_vars : false, ie8 : false, if_return : !false_by_default, @@ -190,6 +191,7 @@ merge(Compressor.prototype, { if (node._squeezed) return node; var was_scope = false; if (node instanceof AST_Scope) { + node = node.hoist_properties(this); node = node.hoist_declarations(this); was_scope = true; } @@ -547,6 +549,7 @@ merge(Compressor.prototype, { } function reset_def(def) { + def.direct_access = false; def.escaped = false; if (def.scope.uses_eval) { def.fixed = false; @@ -604,15 +607,19 @@ merge(Compressor.prototype, { || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope || parent instanceof AST_VarDef && node === parent.value) { d.escaped = true; + return; } else if (parent instanceof AST_Array || parent instanceof AST_Object) { mark_escaped(d, parent, parent, level + 1); } else if (parent instanceof AST_PropAccess && node === parent.expression) { - mark_escaped(d, parent, read_property(value, parent.property), level + 1); + value = read_property(value, parent.property); + mark_escaped(d, parent, value, level + 1); + if (value) return; } + if (level == 0) d.direct_access = true; } }); - AST_SymbolRef.DEFMETHOD("fixed_value", function() { + AST_Symbol.DEFMETHOD("fixed_value", function() { var fixed = this.definition().fixed; if (!fixed || fixed instanceof AST_Node) return fixed; return fixed(); @@ -2478,11 +2485,11 @@ merge(Compressor.prototype, { })); } switch (body.length) { - case 0: + case 0: return in_list ? MAP.skip : make_node(AST_EmptyStatement, node); - case 1: + case 1: return body[0]; - default: + default: return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { body: body }); @@ -2678,6 +2685,71 @@ merge(Compressor.prototype, { return self; }); + AST_Scope.DEFMETHOD("hoist_properties", function(compressor){ + var self = this; + if (!compressor.option("hoist_props") || compressor.has_directive("use asm")) return self; + var defs_by_id = Object.create(null); + var var_names = Object.create(null); + self.enclosed.forEach(function(def) { + var_names[def.name] = true; + }); + self.variables.each(function(def, name) { + var_names[name] = true; + }); + return self.transform(new TreeTransformer(function(node) { + if (node instanceof AST_VarDef) { + var sym = node.name, def, value; + if (sym.scope === self + && !(def = sym.definition()).escaped + && !def.single_use + && !def.direct_access + && (value = sym.fixed_value()) === node.value + && value instanceof AST_Object) { + var defs = new Dictionary(); + var assignments = []; + value.properties.forEach(function(prop) { + assignments.push(make_node(AST_VarDef, node, { + name: make_sym(prop.key), + value: prop.value + })); + }); + defs_by_id[def.id] = defs; + return MAP.splice(assignments); + } + } + if (node instanceof AST_PropAccess && node.expression instanceof AST_SymbolRef) { + var defs = defs_by_id[node.expression.definition().id]; + if (defs) { + var key = node.property; + if (key instanceof AST_Node) key = key.getValue(); + var def = defs.get(key); + var sym = make_node(AST_SymbolRef, node, { + name: def.name, + scope: node.expression.scope, + thedef: def + }); + sym.reference({}); + return sym; + } + } + + function make_sym(key) { + var prefix = sym.name + "_" + key.toString().replace(/[^a-z_$]+/ig, "_"); + var name = prefix; + for (var i = 0; var_names[name]; i++) name = prefix + "$" + i; + var new_var = make_node(sym.CTOR, sym, { + name: name, + scope: self + }); + var def = self.def_variable(new_var); + defs.set(key, def); + self.enclosed.push(def); + var_names[name] = true; + return new_var; + } + })); + }); + // drop_side_effect_free() // remove side-effect-free parts which only affects return value (function(def){ diff --git a/test/compress/hoist_props.js b/test/compress/hoist_props.js new file mode 100644 index 00000000..2e8343a6 --- /dev/null +++ b/test/compress/hoist_props.js @@ -0,0 +1,371 @@ +issue_2377_1: { + options = { + evaluate: true, + inline: true, + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var obj = { + foo: 1, + bar: 2, + square: function(x) { + return x * x; + }, + cube: function(x) { + return x * x * x; + }, + }; + console.log(obj.foo, obj.cube(3)); + } + expect: { + var obj_foo = 1, obj_cube = function(x) { + return x * x * x; + }; + console.log(obj_foo, obj_cube(3)); + } + expect_stdout: "1 27" +} + +issue_2377_2: { + options = { + evaluate: true, + inline: true, + hoist_props: true, + passes: 2, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var obj = { + foo: 1, + bar: 2, + square: function(x) { + return x * x; + }, + cube: function(x) { + return x * x * x; + }, + }; + console.log(obj.foo, obj.cube(3)); + } + expect: { + console.log(1, function(x) { + return x * x * x; + }(3)); + } + expect_stdout: "1 27" +} + +issue_2377_3: { + options = { + evaluate: true, + inline: true, + hoist_props: true, + passes: 3, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var obj = { + foo: 1, + bar: 2, + square: function(x) { + return x * x; + }, + cube: function(x) { + return x * x * x; + }, + }; + console.log(obj.foo, obj.cube(3)); + } + expect: { + console.log(1, 27); + } + expect_stdout: "1 27" +} + +direct_access_1: { + options = { + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var a = 0; + var obj = { + a: 1, + b: 2, + }; + for (var k in obj) a++; + console.log(a, obj.a); + } + expect: { + var a = 0; + var obj = { + a: 1, + b: 2, + }; + for (var k in obj) a++; + console.log(a, obj.a); + } + expect_stdout: "2 1" +} + +direct_access_2: { + options = { + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { a: 1 }; + var f = function(k) { + if (o[k]) return "PASS"; + }; + console.log(f("a")); + } + expect: { + var o = { a: 1 }; + console.log(function(k) { + if (o[k]) return "PASS"; + }("a")); + } + expect_stdout: "PASS" +} + +direct_access_3: { + options = { + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { a: 1 }; + o.b; + console.log(o.a); + } + expect: { + var o = { a: 1 }; + o.b; + console.log(o.a); + } + expect_stdout: "1" +} + +single_use: { + options = { + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var obj = { + bar: function() { + return 42; + }, + }; + console.log(obj.bar()); + } + expect: { + console.log({ + bar: function() { + return 42; + }, + }.bar()); + } +} + +name_collision_1: { + options = { + hoist_props: true, + reduce_vars: true, + toplevel: true, + } + input: { + var obj_foo = 1; + var obj_bar = 2; + function f() { + var obj = { + foo: 3, + bar: 4, + "b-r": 5, + "b+r": 6, + "b!r": 7, + }; + console.log(obj_foo, obj.foo, obj.bar, obj["b-r"], obj["b+r"], obj["b!r"]); + } + f(); + } + expect: { + var obj_foo = 1; + var obj_bar = 2; + function f() { + var obj_foo$0 = 3, + obj_bar = 4, + obj_b_r = 5, + obj_b_r$0 = 6, + obj_b_r$1 = 7; + console.log(obj_foo, obj_foo$0, obj_bar, obj_b_r, obj_b_r$0, obj_b_r$1); + } + f(); + } + expect_stdout: "1 3 4 5 6 7" +} + +name_collision_2: { + options = { + hoist_props: true, + reduce_vars: true, + toplevel: true, + } + input: { + var o = { + p: 1, + 0: function(x) { + return x; + }, + 1: function(x) { + return x + 1; + } + }, o__$0 = 2, o__$1 = 3; + console.log(o.p === o.p, o[0](4), o[1](5), o__$0, o__$1); + } + expect: { + var o_p = 1, + o__ = function(x) { + return x; + }, + o__$2 = function(x) { + return x + 1; + }, + o__$0 = 2, + o__$1 = 3; + console.log(o_p === o_p, o__(4), o__$2(5), o__$0, o__$1); + } + expect_stdout: "true 4 6 2 3" +} + +name_collision_3: { + options = { + hoist_props: true, + reduce_vars: true, + toplevel: true, + } + input: { + var o = { + p: 1, + 0: function(x) { + return x; + }, + 1: function(x) { + return x + 1; + } + }, o__$0 = 2, o__$1 = 3; + console.log(o.p === o.p, o[0](4), o[1](5)); + } + expect: { + var o_p = 1, + o__ = function(x) { + return x; + }, + o__$2 = function(x) { + return x + 1; + }, + o__$0 = 2, + o__$1 = 3; + console.log(o_p === o_p, o__(4), o__$2(5)); + } + expect_stdout: "true 4 6" +} + +contains_this_1: { + options = { + evaluate: true, + hoist_props: true, + inline: true, + passes: 2, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { + u: function() { + return this === this; + }, + p: 1 + }; + console.log(o.p, o.p); + } + expect: { + console.log(1, 1); + } + expect_stdout: "1 1" +} + +contains_this_2: { + options = { + evaluate: true, + hoist_props: true, + inline: true, + passes: 2, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { + u: function() { + return this === this; + }, + p: 1 + }; + console.log(o.p, o.p, o.u); + } + expect: { + console.log(1, 1, function() { + return this === this; + }); + } + expect_stdout: true +} + +contains_this_3: { + options = { + evaluate: true, + hoist_props: true, + inline: true, + passes: 2, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { + u: function() { + return this === this; + }, + p: 1 + }; + console.log(o.p, o.p, o.u()); + } + expect: { + var o = { + u: function() { + return this === this; + }, + p: 1 + }; + console.log(o.p, o.p, o.u()); + } + expect_stdout: "1 1 true" +} diff --git a/test/ufuzz.json b/test/ufuzz.json index cb014b12..0d737d31 100644 --- a/test/ufuzz.json +++ b/test/ufuzz.json @@ -16,11 +16,9 @@ {}, { "compress": { - "toplevel": true + "hoist_props": true }, - "mangle": { - "toplevel": true - } + "toplevel": true }, { "compress": { From ae67a4985073dcdaa2788c86e576202923514e0d Mon Sep 17 00:00:00 2001 From: kzc Date: Wed, 25 Oct 2017 02:03:43 -0400 Subject: [PATCH 11/16] document compress option `hoist_props` (#2399) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 1c4d6114..0d1e2368 100644 --- a/README.md +++ b/README.md @@ -632,6 +632,12 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `hoist_funs` (default: `true`) -- hoist function declarations +- `hoist_props` (default: `false`) -- hoist properties from constant object and + array literals into regular variables subject to a set of constraints. For example: + `var o={p:1, q:2}; f(o.p, o.q);` is converted to `f(1, 2);`. Note: `hoist_props` + works best with `mangle` enabled, the `compress` option `passes` set to `2` or higher, + and the `compress` option `toplevel` enabled. + - `hoist_vars` (default: `false`) -- hoist `var` declarations (this is `false` by default because it seems to increase the size of the output in general) From ee082ace1b69bff228ff43065333b8703c0505dc Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 26 Oct 2017 01:16:12 +0800 Subject: [PATCH 12/16] compress self comparisons (#2398) --- lib/compress.js | 17 +++++++++++++++-- test/compress/comparing.js | 39 +++++++++++++++++++++++++++++++++++++- test/compress/evaluate.js | 37 ++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 9f410718..073399b5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -581,7 +581,7 @@ merge(Compressor.prototype, { if (!value && props[i].key === key) value = props[i].value; } } - return value instanceof AST_SymbolRef ? value.fixed_value() : value; + return value instanceof AST_SymbolRef && value.fixed_value() || value; } function is_modified(node, value, level, immutable) { @@ -3824,6 +3824,11 @@ merge(Compressor.prototype, { }); var commutativeOperators = makePredicate("== === != !== * & | ^"); + function is_object(node) { + return node instanceof AST_Array + || node instanceof AST_Lambda + || node instanceof AST_Object; + } OPT(AST_Binary, function(self, compressor){ function reversible() { @@ -3859,7 +3864,8 @@ merge(Compressor.prototype, { case "!==": if ((self.left.is_string(compressor) && self.right.is_string(compressor)) || (self.left.is_number(compressor) && self.right.is_number(compressor)) || - (self.left.is_boolean() && self.right.is_boolean())) { + (self.left.is_boolean() && self.right.is_boolean()) || + self.left.equivalent_to(self.right)) { self.operator = self.operator.substr(0, 2); } // XXX: intentionally falling down to the next case @@ -3879,6 +3885,13 @@ merge(Compressor.prototype, { if (self.operator.length == 2) self.operator += "="; } } + // obj !== obj => false + else if (self.left instanceof AST_SymbolRef + && self.right instanceof AST_SymbolRef + && self.left.definition() === self.right.definition() + && is_object(self.left.fixed_value())) { + return make_node(self.operator[0] == "=" ? AST_True : AST_False, self); + } break; } if (compressor.option("booleans") && self.operator == "+" && compressor.in_boolean_context()) { diff --git a/test/compress/comparing.js b/test/compress/comparing.js index c51fac31..11804cbb 100644 --- a/test/compress/comparing.js +++ b/test/compress/comparing.js @@ -73,4 +73,41 @@ dont_change_in_or_instanceof_expressions: { 1 instanceof 1; null instanceof null; } -} \ No newline at end of file +} + +self_comparison_1: { + options = { + comparisons: true, + } + input: { + a === a; + a !== b; + b.c === a.c; + b.c !== b.c; + } + expect: { + a == a; + a !== b; + b.c === a.c; + b.c != b.c; + } +} + +self_comparison_2: { + options = { + comparisons: true, + reduce_vars: true, + toplevel: true, + } + input: { + function f() {} + var o = {}; + console.log(f != f, o === o); + } + expect: { + function f() {} + var o = {}; + console.log(false, true); + } + expect_stdout: "false true" +} diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index fe9464bc..dc8ceb62 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -1195,3 +1195,40 @@ issue_2231_2: { } expect_stdout: true } + +self_comparison_1: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + var o = { n: NaN }; + console.log(o.n == o.n, o.n === o.n, o.n != o.n, o.n !== o.n, typeof o.n); + } + expect: { + console.log(false, false, true, true, "number"); + } + expect_stdout: "false false true true 'number'" +} + +self_comparison_2: { + options = { + evaluate: true, + hoist_props: true, + passes: 2, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { n: NaN }; + console.log(o.n == o.n, o.n === o.n, o.n != o.n, o.n !== o.n, typeof o.n); + } + expect: { + console.log(false, false, true, true, "number"); + } + expect_stdout: "false false true true 'number'" +} From 9b0f86f5a136f57dece826c3ec9b632a0d0cb53a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 27 Oct 2017 02:33:37 +0800 Subject: [PATCH 13/16] fix `reduce_vars` on `AST_Array.length` (#2404) --- lib/compress.js | 4 ++- test/compress/arrays.js | 57 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 073399b5..a2dd243f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -572,7 +572,9 @@ merge(Compressor.prototype, { if (key instanceof AST_Node) return null; var value; if (obj instanceof AST_Array) { - value = obj.elements[key]; + var elements = obj.elements; + if (key == "length") return make_node_from_constant(elements.length, obj); + if (typeof key == "number" && key in elements) value = elements[key]; } else if (obj instanceof AST_Object) { var props = obj.properties; for (var i = props.length; --i >= 0;) { diff --git a/test/compress/arrays.js b/test/compress/arrays.js index 539dfb04..27400901 100644 --- a/test/compress/arrays.js +++ b/test/compress/arrays.js @@ -131,6 +131,7 @@ for_loop: { evaluate: true, reduce_vars: true, unsafe: true, + unused: true, }; input: { function f0() { @@ -166,7 +167,7 @@ for_loop: { function f1() { var a = [1, 2, 3]; var b = 0; - for (var i = 0, len = a.length; i < len; i++) + for (var i = 0; i < 3; i++) b += a[i]; return b; } @@ -180,3 +181,57 @@ for_loop: { } expect_stdout: "6 6 4" } + +index: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + var a = [ 1, 2 ]; + console.log(a[0], a[1]); + } + expect: { + console.log(1, 2); + } + expect_stdout: "1 2" +} + +length: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + var a = [ 1, 2 ]; + console.log(a.length); + } + expect: { + console.log(2); + } + expect_stdout: "2" +} + +index_length: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, + unused: true, + } + input: { + var a = [ 1, 2 ]; + console.log(a[0], a.length); + } + expect: { + console.log(1, 2); + } + expect_stdout: "1 2" +} From 31f82091930b39ebb5d9358951c39ae20cf8057e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 27 Oct 2017 14:28:09 +0800 Subject: [PATCH 14/16] remove dead code (#2405) --- lib/compress.js | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index a2dd243f..4817ec5f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1647,35 +1647,6 @@ merge(Compressor.prototype, { && unaryPrefix(this.operator); } }); - // Obtain the constant value of an expression already known to be constant. - // Result only valid iff this.is_constant() is true. - AST_Node.DEFMETHOD("constant_value", function(compressor){ - // Accomodate when option evaluate=false. - if (this instanceof AST_Constant && !(this instanceof AST_RegExp)) { - return this.value; - } - // Accomodate the common constant expressions !0 and -1 when option evaluate=false. - if (this instanceof AST_UnaryPrefix - && this.expression instanceof AST_Constant) switch (this.operator) { - case "!": - return !this.expression.value; - case "~": - return ~this.expression.value; - case "-": - return -this.expression.value; - case "+": - return +this.expression.value; - default: - throw new Error(string_template("Cannot evaluate unary expression {value}", { - value: this.print_to_string() - })); - } - var result = this.evaluate(compressor); - if (result !== this) { - return result; - } - throw new Error(string_template("Cannot evaluate constant [{file}:{line},{col}]", this.start)); - }); def(AST_Statement, function(){ throw new Error(string_template("Cannot evaluate a statement [{file}:{line},{col}]", this.start)); }); From 8428326ea120dece51b70d7bba63dda8eda14fd6 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 29 Oct 2017 04:11:26 +0800 Subject: [PATCH 15/16] enhance `properties` (#2412) - trim array items only if `side_effects` - extend to non-identifier properties --- lib/compress.js | 153 ++++++++++++++++++++--------------- test/compress/evaluate.js | 2 + test/compress/properties.js | 51 ++++++++---- test/compress/reduce_vars.js | 1 + 4 files changed, 125 insertions(+), 82 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 4817ec5f..a1db985c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4537,59 +4537,72 @@ merge(Compressor.prototype, { }); OPT(AST_Sub, function(self, compressor){ + var expr = self.expression; + var prop = self.property; if (compressor.option("properties")) { - var prop = self.property; - if (prop instanceof AST_String) { - prop = prop.getValue(); - if (is_identifier_string(prop)) { + var key = prop.evaluate(compressor); + if (key !== prop) { + var property = "" + key; + if (is_identifier_string(property) + && property.length <= prop.print_to_string().length + 1) { return make_node(AST_Dot, self, { - expression : self.expression, - property : prop + expression: expr, + property: property }).optimize(compressor); } - var v = parseFloat(prop); - if (!isNaN(v) && v.toString() == prop) { - self.property = make_node(AST_Number, self.property, { - value: v - }); - } - } - if (prop instanceof AST_Number && self.expression instanceof AST_Array) { - prop = prop.getValue(); - var elements = self.expression.elements; - if (prop in elements) { - var flatten = true; - var values = []; - for (var i = elements.length; --i > prop;) { - var value = elements[i].drop_side_effect_free(compressor); - if (value) { - values.unshift(value); - if (flatten && value.has_side_effects(compressor)) flatten = false; - } + if (!(prop instanceof AST_Number)) { + var value = parseFloat(property); + if (value.toString() == property) { + prop = self.property = make_node(AST_Number, prop, { + value: value + }); } - var retValue = elements[prop]; - retValue = retValue instanceof AST_Hole ? make_node(AST_Undefined, retValue) : retValue; - if (!flatten) values.unshift(retValue); - while (--i >= 0) { - var value = elements[i].drop_side_effect_free(compressor); - if (value) values.unshift(value); - else prop--; - } - if (flatten) { - values.push(retValue); - return make_sequence(self, values).optimize(compressor); - } else return make_node(AST_Sub, self, { - expression: make_node(AST_Array, self.expression, { - elements: values - }), - property: make_node(AST_Number, self.property, { - value: prop - }) - }); } } } if (is_lhs(self, compressor.parent())) return self; + if (compressor.option("properties") && key !== prop) { + var node = self.flatten_object(property); + if (node) { + expr = self.expression = node.expression; + prop = self.property = node.property; + } + } + if (compressor.option("properties") && compressor.option("side_effects") + && prop instanceof AST_Number && expr instanceof AST_Array) { + var index = prop.getValue(); + var elements = expr.elements; + if (index in elements) { + var flatten = true; + var values = []; + for (var i = elements.length; --i > index;) { + var value = elements[i].drop_side_effect_free(compressor); + if (value) { + values.unshift(value); + if (flatten && value.has_side_effects(compressor)) flatten = false; + } + } + var retValue = elements[index]; + retValue = retValue instanceof AST_Hole ? make_node(AST_Undefined, retValue) : retValue; + if (!flatten) values.unshift(retValue); + while (--i >= 0) { + var value = elements[i].drop_side_effect_free(compressor); + if (value) values.unshift(value); + else index--; + } + if (flatten) { + values.push(retValue); + return make_sequence(self, values).optimize(compressor); + } else return make_node(AST_Sub, self, { + expression: make_node(AST_Array, expr, { + elements: values + }), + property: make_node(AST_Number, prop, { + value: index + }) + }); + } + } var ev = self.evaluate(compressor); if (ev !== self) { ev = make_node_from_constant(ev, self).optimize(compressor); @@ -4609,6 +4622,33 @@ merge(Compressor.prototype, { return result; }); + AST_PropAccess.DEFMETHOD("flatten_object", function(key) { + var expr = this.expression; + if (expr instanceof AST_Object) { + var props = expr.properties; + for (var i = props.length; --i >= 0;) { + var prop = props[i]; + if ("" + prop.key == key) { + if (!all(props, function(prop) { + return prop instanceof AST_ObjectKeyVal; + })) break; + var value = prop.value; + if (value instanceof AST_Function && value.contains_this()) break; + return make_node(AST_Sub, this, { + expression: make_node(AST_Array, expr, { + elements: props.map(function(prop) { + return prop.value; + }) + }), + property: make_node(AST_Number, this, { + value: i + }) + }); + } + } + } + }); + OPT(AST_Dot, function(self, compressor){ var def = self.resolve_defines(compressor); if (def) { @@ -4637,28 +4677,9 @@ merge(Compressor.prototype, { } } if (is_lhs(self, compressor.parent())) return self; - if (compressor.option("properties") && self.expression instanceof AST_Object) { - var props = self.expression.properties; - for (var i = props.length; --i >= 0;) { - var prop = props[i]; - if (prop.key === self.property) { - if (!all(props, function(prop) { - return prop instanceof AST_ObjectKeyVal; - })) break; - var value = prop.value; - if (value instanceof AST_Function && value.contains_this()) break; - return make_node(AST_Sub, self, { - expression: make_node(AST_Array, self.expression, { - elements: props.map(function(prop) { - return prop.value; - }) - }), - property: make_node(AST_Number, self, { - value: i - }) - }).optimize(compressor); - } - } + if (compressor.option("properties")) { + var node = self.flatten_object(self.property); + if (node) return node.optimize(compressor); } var ev = self.evaluate(compressor); if (ev !== self) { diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index dc8ceb62..64728c06 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -390,6 +390,7 @@ prop_function: { options = { evaluate: true, properties: true, + side_effects: true, } input: { console.log( @@ -634,6 +635,7 @@ prototype_function: { options = { evaluate: true, properties: true, + side_effects: true, } input: { var a = ({valueOf: 0}) < 1; diff --git a/test/compress/properties.js b/test/compress/properties.js index f435d371..1b5e7fc7 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -1,7 +1,8 @@ keep_properties: { options = { - properties: false - }; + evaluate: true, + properties: false, + } input: { a["foo"] = "bar"; } @@ -12,6 +13,7 @@ keep_properties: { dot_properties: { options = { + evaluate: true, properties: true, } beautify = { @@ -37,6 +39,7 @@ dot_properties: { dot_properties_es5: { options = { + evaluate: true, properties: true, } beautify = { @@ -61,8 +64,8 @@ dot_properties_es5: { sub_properties: { options = { evaluate: true, - properties: true - }; + properties: true, + } input: { a[0] = 0; a["0"] = 1; @@ -81,18 +84,18 @@ sub_properties: { a[3.14] = 3; a.if = 4; a["foo bar"] = 5; - a[NaN] = 6; - a[null] = 7; + a.NaN = 6; + a.null = 7; a[void 0] = 8; } } evaluate_array_length: { options = { + evaluate: true, properties: true, unsafe: true, - evaluate: true - }; + } input: { a = [1, 2, 3].length; a = [1, 2, 3].join()["len" + "gth"]; @@ -109,10 +112,10 @@ evaluate_array_length: { evaluate_string_length: { options = { + evaluate: true, properties: true, unsafe: true, - evaluate: true - }; + } input: { a = "foo".length; a = ("foo" + "bar")["len" + "gth"]; @@ -151,7 +154,8 @@ mangle_properties: { mangle_unquoted_properties: { options = { - properties: false + evaluate: true, + properties: false, } mangle = { properties: { @@ -249,7 +253,8 @@ mangle_debug_suffix: { mangle_debug_suffix_keep_quoted: { options = { - properties: false + evaluate: true, + properties: false, } mangle = { properties: { @@ -833,18 +838,29 @@ lhs_prop_2: { unused: true, } input: { + [1][0] = 42; + (function(a) { + a.b = "g"; + })("abc"); (function(a) { a[2] = "g"; - })("abc"); + })("def"); + (function(a) { + a[""] = "g"; + })("ghi"); } expect: { - "abc"[2] = "g"; + [1][0] = 42; + "abc".b = "g"; + "def"[2] = "g"; + "ghi"[""] = "g"; } } literal_duplicate_key_side_effects: { options = { properties: true, + side_effects: true, } input: { console.log({ @@ -864,6 +880,7 @@ prop_side_effects_1: { inline: true, properties: true, reduce_vars: true, + side_effects: true, toplevel: true, unused: true, } @@ -899,6 +916,7 @@ prop_side_effects_2: { passes: 2, properties: true, reduce_vars: true, + side_effects: true, toplevel: true, unused: true, } @@ -906,11 +924,11 @@ prop_side_effects_2: { var C = 1; console.log(C); var obj = { - bar: function() { + "": function() { return C + C; } }; - console.log(obj.bar()); + console.log(obj[""]()); } expect: { console.log(1); @@ -974,6 +992,7 @@ accessor_2: { array_hole: { options = { properties: true, + side_effects: true, } input: { console.log( diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index ac4fa40b..d7fdee18 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2973,6 +2973,7 @@ obj_var_2: { passes: 2, properties: true, reduce_vars: true, + side_effects: true, toplevel: true, unsafe: true, unused: true, From 2fd927a7ccfb55de415bf1faafb45e5006ca9984 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 29 Oct 2017 12:38:10 +0800 Subject: [PATCH 16/16] v3.1.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b93d84b..9f41493d 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.1.5", + "version": "3.1.6", "engines": { "node": ">=0.8.0" },