From: Alex Lam S.L Date: Sun, 30 Apr 2017 14:52:36 +0000 (+0800) Subject: enforce `toplevel` on other compress options (#1855) X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=2cb55b2ad0119852bc8714401992724d4fdb224d;p=UglifyJS.git enforce `toplevel` on other compress options (#1855) Respect "funcs" and "vars" properly. fixes #1850 --- diff --git a/lib/compress.js b/lib/compress.js index 0f4dd255..4e86a307 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -109,6 +109,14 @@ function Compressor(options, false_by_default) { return top_retain.indexOf(def.name) >= 0; }; } + var toplevel = this.options["toplevel"]; + if (typeof toplevel == "string") { + this.toplevel.funcs = /funcs/.test(toplevel); + this.toplevel.vars = /vars/.test(toplevel); + } else { + this.toplevel = toplevel ? return_true : return_false; + this.toplevel.funcs = this.toplevel.vars = toplevel; + } var sequences = this.options["sequences"]; this.sequences_limit = sequences == 1 ? 800 : sequences | 0; this.warnings_produced = {}; @@ -117,6 +125,12 @@ function Compressor(options, false_by_default) { Compressor.prototype = new TreeTransformer; merge(Compressor.prototype, { option: function(key) { return this.options[key] }, + toplevel: function(def) { + for (var i = 0, len = def.orig.length; i < len; i++) + if (!this.toplevel[def.orig[i] instanceof AST_SymbolDefun ? "funcs" : "vars"]) + return false; + return true; + }, compress: function(node) { if (this.option("expression")) { node.process_expression(true); @@ -249,7 +263,6 @@ merge(Compressor.prototype, { AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan) { var reduce_vars = rescan && compressor.option("reduce_vars"); - var toplevel = compressor.option("toplevel"); var safe_ids = Object.create(null); var suppressor = new TreeWalker(function(node) { if (node instanceof AST_Symbol) { @@ -310,7 +323,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Defun) { var d = node.name.definition(); - if (!toplevel && d.global || safe_to_read(d)) { + if (d.global && !compressor.toplevel(d) || safe_to_read(d)) { d.fixed = false; } else { d.fixed = node; @@ -469,7 +482,7 @@ merge(Compressor.prototype, { } function reset_def(def) { - if (toplevel || !def.global || def.orig[0] instanceof AST_SymbolConst) { + if (!def.global || def.orig[0] instanceof AST_SymbolConst || compressor.toplevel(def)) { def.fixed = undefined; } else { def.fixed = false; @@ -643,7 +656,6 @@ merge(Compressor.prototype, { // in the statement with the var value and then erase the var definition. var scope = compressor.find_parent(AST_Scope); - var toplevel = compressor.option("toplevel"); var stat_index; var prev_stat_index; var def_stat_index; @@ -694,7 +706,8 @@ merge(Compressor.prototype, { // Only interested in cases with just one reference to the variable. var def = var_decl.name.definition(); if (def.references.length !== 1 - || var_name == "arguments" || (!toplevel && def.global)) { + || var_name == "arguments" + || def.global && !compressor.toplevel(def)) { side_effects_encountered = true; continue; } @@ -1825,289 +1838,283 @@ merge(Compressor.prototype, { }); AST_Scope.DEFMETHOD("drop_unused", function(compressor){ + if (!compressor.option("unused")) return; + if (compressor.has_directive("use asm")) return; var self = this; - if (compressor.has_directive("use asm")) return self; - var toplevel = compressor.option("toplevel"); - if (compressor.option("unused") - && (!(self instanceof AST_Toplevel) || toplevel) - && !self.uses_eval - && !self.uses_with) { - var assign_as_unused = !/keep_assign/.test(compressor.option("unused")); - var drop_funcs = /funcs/.test(toplevel); - var drop_vars = /vars/.test(toplevel); - if (!(self instanceof AST_Toplevel) || toplevel == true) { - drop_funcs = drop_vars = true; - } - var in_use = []; - var in_use_ids = Object.create(null); // avoid expensive linear scans of in_use - if (self instanceof AST_Toplevel && compressor.top_retain) { - self.variables.each(function(def) { - if (compressor.top_retain(def) && !(def.id in in_use_ids)) { - in_use_ids[def.id] = true; - in_use.push(def); + 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")); + var in_use = []; + var in_use_ids = Object.create(null); // avoid expensive linear scans of in_use + if (self instanceof AST_Toplevel && compressor.top_retain) { + self.variables.each(function(def) { + if (compressor.top_retain(def) && !(def.id in in_use_ids)) { + in_use_ids[def.id] = true; + in_use.push(def); + } + }); + } + var var_defs_by_id = new Dictionary(); + var initializations = new Dictionary(); + // pass 1: find out which symbols are directly used in + // this scope (not in nested scopes). + var scope = this; + var tw = new TreeWalker(function(node, descend){ + if (node !== self) { + if (node instanceof AST_Defun) { + if (!drop_funcs && scope === self) { + var node_def = node.name.definition(); + if (!(node_def.id in in_use_ids)) { + in_use_ids[node_def.id] = true; + in_use.push(node_def); + } } - }); - } - var var_defs_by_id = new Dictionary(); - var initializations = new Dictionary(); - // pass 1: find out which symbols are directly used in - // this scope (not in nested scopes). - var scope = this; - var tw = new TreeWalker(function(node, descend){ - if (node !== self) { - if (node instanceof AST_Defun) { - if (!drop_funcs && scope === self) { - var node_def = node.name.definition(); + initializations.add(node.name.name, node); + return true; // don't go in nested scopes + } + if (node instanceof AST_Definitions && scope === self) { + node.definitions.forEach(function(def){ + var node_def = def.name.definition(); + if (def.name instanceof AST_SymbolVar) { + var_defs_by_id.add(node_def.id, def); + } + if (!drop_vars) { if (!(node_def.id in in_use_ids)) { in_use_ids[node_def.id] = true; in_use.push(node_def); } } - initializations.add(node.name.name, node); - return true; // don't go in nested scopes - } - if (node instanceof AST_Definitions && scope === self) { - node.definitions.forEach(function(def){ - var node_def = def.name.definition(); - if (def.name instanceof AST_SymbolVar) { - var_defs_by_id.add(node_def.id, def); - } - if (!drop_vars) { - if (!(node_def.id in in_use_ids)) { - in_use_ids[node_def.id] = true; - in_use.push(node_def); - } - } - if (def.value) { - initializations.add(def.name.name, def.value); - if (def.value.has_side_effects(compressor)) { - def.value.walk(tw); - } + if (def.value) { + initializations.add(def.name.name, def.value); + if (def.value.has_side_effects(compressor)) { + def.value.walk(tw); } - }); - return true; - } - if (assign_as_unused - && node instanceof AST_Assign - && node.operator == "=" - && node.left instanceof AST_SymbolRef - && scope === self) { - node.right.walk(tw); - return true; - } - if (node instanceof AST_SymbolRef) { - var node_def = node.definition(); - if (!(node_def.id in in_use_ids)) { - in_use_ids[node_def.id] = true; - in_use.push(node_def); } - return true; - } - if (node instanceof AST_Scope) { - var save_scope = scope; - scope = node; - descend(); - scope = save_scope; - return true; + }); + return true; + } + if (assign_as_unused + && node instanceof AST_Assign + && node.operator == "=" + && node.left instanceof AST_SymbolRef + && scope === self) { + node.right.walk(tw); + return true; + } + if (node instanceof AST_SymbolRef) { + var node_def = node.definition(); + if (!(node_def.id in in_use_ids)) { + in_use_ids[node_def.id] = true; + in_use.push(node_def); } + return true; } - }); - self.walk(tw); - // pass 2: for every used symbol we need to walk its - // initialization code to figure out if it uses other - // symbols (that may not be in_use). - for (var i = 0; i < in_use.length; ++i) { - in_use[i].orig.forEach(function(decl){ - // undeclared globals will be instanceof AST_SymbolRef - var init = initializations.get(decl.name); - if (init) init.forEach(function(init){ - var tw = new TreeWalker(function(node){ - if (node instanceof AST_SymbolRef) { - var node_def = node.definition(); - if (!(node_def.id in in_use_ids)) { - in_use_ids[node_def.id] = true; - in_use.push(node_def); - } + if (node instanceof AST_Scope) { + var save_scope = scope; + scope = node; + descend(); + scope = save_scope; + return true; + } + } + }); + self.walk(tw); + // pass 2: for every used symbol we need to walk its + // initialization code to figure out if it uses other + // symbols (that may not be in_use). + for (var i = 0; i < in_use.length; ++i) { + in_use[i].orig.forEach(function(decl){ + // undeclared globals will be instanceof AST_SymbolRef + var init = initializations.get(decl.name); + if (init) init.forEach(function(init){ + var tw = new TreeWalker(function(node){ + if (node instanceof AST_SymbolRef) { + var node_def = node.definition(); + if (!(node_def.id in in_use_ids)) { + in_use_ids[node_def.id] = true; + in_use.push(node_def); } - }); - init.walk(tw); + } }); + init.walk(tw); }); - } - // pass 3: we should drop declarations not in_use - var tt = new TreeTransformer( - function before(node, descend, in_list) { - if (node instanceof AST_Function - && node.name - && !compressor.option("keep_fnames")) { - var def = node.name.definition(); - // any declarations with same name will overshadow - // name of this anonymous function and can therefore - // never be used anywhere - if (!(def.id in in_use_ids) || def.orig.length > 1) - node.name = null; - } - if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { - var trim = !compressor.option("keep_fargs"); - for (var a = node.argnames, i = a.length; --i >= 0;) { - var sym = a[i]; - if (!(sym.definition().id in in_use_ids)) { - sym.__unused = true; - if (trim) { - a.pop(); - compressor[sym.unreferenced() ? "warn" : "info"]("Dropping unused function argument {name} [{file}:{line},{col}]", template(sym)); - } - } - else { - trim = false; + }); + } + // pass 3: we should drop declarations not in_use + var tt = new TreeTransformer( + function before(node, descend, in_list) { + if (node instanceof AST_Function + && node.name + && !compressor.option("keep_fnames")) { + var def = node.name.definition(); + // any declarations with same name will overshadow + // name of this anonymous function and can therefore + // never be used anywhere + if (!(def.id in in_use_ids) || def.orig.length > 1) + node.name = null; + } + if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { + var trim = !compressor.option("keep_fargs"); + for (var a = node.argnames, i = a.length; --i >= 0;) { + var sym = a[i]; + if (!(sym.definition().id in in_use_ids)) { + sym.__unused = true; + if (trim) { + a.pop(); + compressor[sym.unreferenced() ? "warn" : "info"]("Dropping unused function argument {name} [{file}:{line},{col}]", template(sym)); } } - } - if (drop_funcs && node instanceof AST_Defun && node !== self) { - if (!(node.name.definition().id in in_use_ids)) { - compressor[node.name.unreferenced() ? "warn" : "info"]("Dropping unused function {name} [{file}:{line},{col}]", template(node.name)); - return make_node(AST_EmptyStatement, node); + else { + trim = false; } - return node; } - if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn && tt.parent().init === node)) { - // place uninitialized names at the start - var body = [], head = [], tail = []; - // for unused names whose initialization has - // side effects, we can cascade the init. code - // into the next one, or next statement. - var side_effects = []; - 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 (def.name instanceof AST_SymbolVar) { - var var_defs = var_defs_by_id.get(sym.id); - if (var_defs.length > 1 && !def.value) { - compressor.warn("Dropping duplicated definition of variable {name} [{file}:{line},{col}]", template(def.name)); - var_defs.splice(var_defs.indexOf(def), 1); - return; - } + } + if (drop_funcs && node instanceof AST_Defun && node !== self) { + if (!(node.name.definition().id in in_use_ids)) { + compressor[node.name.unreferenced() ? "warn" : "info"]("Dropping unused function {name} [{file}:{line},{col}]", template(node.name)); + return make_node(AST_EmptyStatement, node); + } + return node; + } + if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn && tt.parent().init === node)) { + // place uninitialized names at the start + var body = [], head = [], tail = []; + // for unused names whose initialization has + // side effects, we can cascade the init. code + // into the next one, or next statement. + var side_effects = []; + 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 (def.name instanceof AST_SymbolVar) { + var var_defs = var_defs_by_id.get(sym.id); + if (var_defs.length > 1 && !def.value) { + compressor.warn("Dropping duplicated definition of variable {name} [{file}:{line},{col}]", template(def.name)); + var_defs.splice(var_defs.indexOf(def), 1); + return; } - if (def.value) { - if (side_effects.length > 0) { - if (tail.length > 0) { - merge_sequence(side_effects, def.value); - def.value = make_sequence(def.value, side_effects); - } else { - body.push(make_node(AST_SimpleStatement, node, { - body: make_sequence(node, side_effects) - })); - } - side_effects = []; + } + if (def.value) { + if (side_effects.length > 0) { + if (tail.length > 0) { + merge_sequence(side_effects, def.value); + def.value = make_sequence(def.value, side_effects); + } else { + body.push(make_node(AST_SimpleStatement, node, { + body: make_sequence(node, side_effects) + })); } - tail.push(def); - } else { - head.push(def); + side_effects = []; } - } else if (sym.orig[0] instanceof AST_SymbolCatch) { - var value = def.value && def.value.drop_side_effect_free(compressor); - if (value) merge_sequence(side_effects, value); - def.value = null; - head.push(def); + tail.push(def); } else { - var value = def.value && def.value.drop_side_effect_free(compressor); - if (value) { - compressor.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]", template(def.name)); - merge_sequence(side_effects, value); - } else { - compressor[def.name.unreferenced() ? "warn" : "info"]("Dropping unused variable {name} [{file}:{line},{col}]", template(def.name)); - } + head.push(def); } - }); - if (head.length == 0 && tail.length == 1 && tail[0].name instanceof AST_SymbolVar) { - var var_defs = var_defs_by_id.get(tail[0].name.definition().id); - if (var_defs.length > 1) { - var def = tail.pop(); - compressor.warn("Converting duplicated definition of variable {name} to assignment [{file}:{line},{col}]", template(def.name)); - var_defs.splice(var_defs.indexOf(def), 1); - side_effects.unshift(make_node(AST_Assign, def, { - operator: "=", - left: make_node(AST_SymbolRef, def.name, def.name), - right: def.value - })); + } else if (sym.orig[0] instanceof AST_SymbolCatch) { + var value = def.value && def.value.drop_side_effect_free(compressor); + if (value) merge_sequence(side_effects, value); + def.value = null; + head.push(def); + } else { + var value = def.value && def.value.drop_side_effect_free(compressor); + if (value) { + compressor.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]", template(def.name)); + merge_sequence(side_effects, value); + } else { + compressor[def.name.unreferenced() ? "warn" : "info"]("Dropping unused variable {name} [{file}:{line},{col}]", template(def.name)); } } - if (head.length > 0 || tail.length > 0) { - node.definitions = head.concat(tail); - body.push(node); - } - if (side_effects.length > 0) { - body.push(make_node(AST_SimpleStatement, node, { - body: make_sequence(node, side_effects) + }); + if (head.length == 0 && tail.length == 1 && tail[0].name instanceof AST_SymbolVar) { + var var_defs = var_defs_by_id.get(tail[0].name.definition().id); + if (var_defs.length > 1) { + var def = tail.pop(); + compressor.warn("Converting duplicated definition of variable {name} to assignment [{file}:{line},{col}]", template(def.name)); + var_defs.splice(var_defs.indexOf(def), 1); + side_effects.unshift(make_node(AST_Assign, def, { + operator: "=", + left: make_node(AST_SymbolRef, def.name, def.name), + right: def.value })); } - switch (body.length) { - case 0: - return in_list ? MAP.skip : make_node(AST_EmptyStatement, node); - case 1: - return body[0]; - default: - return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { - body: body - }); - } - } - if (drop_vars && assign_as_unused - && node instanceof AST_Assign - && node.operator == "=" - && node.left instanceof AST_SymbolRef) { - var def = node.left.definition(); - if (!(def.id in in_use_ids) - && self.variables.get(def.name) === def) { - return maintain_this_binding(tt.parent(), node, node.right.transform(tt)); - } } - // certain combination of unused name + side effect leads to: - // https://github.com/mishoo/UglifyJS2/issues/44 - // https://github.com/mishoo/UglifyJS2/issues/1830 - // https://github.com/mishoo/UglifyJS2/issues/1838 - // that's an invalid AST. - // We fix it at this stage by moving the `var` outside the `for`. - if (node instanceof AST_For) { - descend(node, this); - if (node.init instanceof AST_BlockStatement) { - var block = node.init; - node.init = block.body.pop(); - block.body.push(node); - return in_list ? MAP.splice(block.body) : block; - } else if (node.init instanceof AST_SimpleStatement) { - node.init = node.init.body; - } else if (is_empty(node.init)) { - node.init = null; - } - return node; + if (head.length > 0 || tail.length > 0) { + node.definitions = head.concat(tail); + body.push(node); + } + if (side_effects.length > 0) { + body.push(make_node(AST_SimpleStatement, node, { + body: make_sequence(node, side_effects) + })); + } + switch (body.length) { + case 0: + return in_list ? MAP.skip : make_node(AST_EmptyStatement, node); + case 1: + return body[0]; + default: + return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { + body: body + }); } - if (node instanceof AST_LabeledStatement && node.body instanceof AST_For) { - descend(node, this); - if (node.body instanceof AST_BlockStatement) { - var block = node.body; - node.body = block.body.pop(); - block.body.push(node); - return in_list ? MAP.splice(block.body) : block; - } - return node; + } + if (drop_vars && assign_as_unused + && node instanceof AST_Assign + && node.operator == "=" + && node.left instanceof AST_SymbolRef) { + var def = node.left.definition(); + if (!(def.id in in_use_ids) + && self.variables.get(def.name) === def) { + return maintain_this_binding(tt.parent(), node, node.right.transform(tt)); + } + } + // certain combination of unused name + side effect leads to: + // https://github.com/mishoo/UglifyJS2/issues/44 + // https://github.com/mishoo/UglifyJS2/issues/1830 + // https://github.com/mishoo/UglifyJS2/issues/1838 + // that's an invalid AST. + // We fix it at this stage by moving the `var` outside the `for`. + if (node instanceof AST_For) { + descend(node, this); + if (node.init instanceof AST_BlockStatement) { + var block = node.init; + node.init = block.body.pop(); + block.body.push(node); + return in_list ? MAP.splice(block.body) : block; + } else if (node.init instanceof AST_SimpleStatement) { + node.init = node.init.body; + } else if (is_empty(node.init)) { + node.init = null; } - if (node instanceof AST_Scope && node !== self) - return node; - - function template(sym) { - return { - name : sym.name, - file : sym.start.file, - line : sym.start.line, - col : sym.start.col - }; + return node; + } + if (node instanceof AST_LabeledStatement && node.body instanceof AST_For) { + descend(node, this); + if (node.body instanceof AST_BlockStatement) { + var block = node.body; + node.body = block.body.pop(); + block.body.push(node); + return in_list ? MAP.splice(block.body) : block; } + return node; } - ); - self.transform(tt); - } + if (node instanceof AST_Scope && node !== self) + return node; + + function template(sym) { + return { + name : sym.name, + file : sym.start.file, + line : sym.start.line, + col : sym.start.col + }; + } + } + ); + self.transform(tt); }); AST_Scope.DEFMETHOD("hoist_declarations", function(compressor){ @@ -3683,7 +3690,7 @@ merge(Compressor.prototype, { } var name = d.name.length; var overhead = 0; - if (compressor.option("unused") && (!d.global || compressor.option("toplevel"))) { + if (compressor.option("unused") && (!d.global || compressor.toplevel(d))) { overhead = (name + 2 + value) / d.references.length; } d.should_replace = value <= name + overhead ? fn : false; diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 57e23891..f7bdcb36 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2327,3 +2327,93 @@ iife_assign: { } expect_stdout: "1" } + +issue_1850_1: { + options = { + reduce_vars: true, + toplevel: false, + unused: true, + } + input: { + function f() { + console.log(a, a, a); + } + var a = 1; + f(); + } + expect: { + function f() { + console.log(a, a, a); + } + var a = 1; + f(); + } + expect_stdout: true +} + +issue_1850_2: { + options = { + reduce_vars: true, + toplevel: "funcs", + unused: true, + } + input: { + function f() { + console.log(a, a, a); + } + var a = 1; + f(); + } + expect: { + var a = 1; + (function() { + console.log(a, a, a); + })(); + } + expect_stdout: true +} + +issue_1850_3: { + options = { + reduce_vars: true, + toplevel: "vars", + unused: true, + } + input: { + function f() { + console.log(a, a, a); + } + var a = 1; + f(); + } + expect: { + function f() { + console.log(a, a, a); + } + var a = 1; + f(); + } + expect_stdout: true +} + +issue_1850_4: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + console.log(a, a, a); + } + var a = 1; + f(); + } + expect: { + var a = 1; + (function() { + console.log(a, a, a); + })(); + } + expect_stdout: true +}