From: Alex Lam S.L Date: Tue, 24 Oct 2017 19:38:11 +0000 (+0800) Subject: implement `hoist_props` (#2396) X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=4178289c382caf2eb3464390370dd1400a23468a;p=UglifyJS.git implement `hoist_props` (#2396) fixes #2377 --- 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": {