From 148047fbbf1951a52e69170edf510c59b3899e6c Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:03:53 +0800 Subject: [PATCH] drop unused: toplevel, assign-only - assign statement does not count towards variable usage by default - only works with assignments on the same scope level as declaration - can be disabled with `unused` set to "keep_assign" - `toplevel` to drop unused top-level variables and/or functions - `top_retain` to whitelist top-level exceptions closes #1450 --- README.md | 10 +- lib/compress.js | 72 ++++++- test/compress/collapse_vars.js | 114 +++++++++- test/compress/drop-unused.js | 379 +++++++++++++++++++++++++++++++++ 4 files changed, 565 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a8b55843..a2eaeae4 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,15 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `loops` -- optimizations for `do`, `while` and `for` loops when we can statically determine the condition -- `unused` -- drop unreferenced functions and variables +- `unused` -- drop unreferenced functions and variables (simple direct variable + assignments do not count as references unless set to `"keep_assign"`) + +- `toplevel` -- drop unreferenced functions (`"funcs"`) and/or variables (`"vars"`) + in the toplevel scope (`false` by default, `true` to drop both unreferenced + functions and variables) + +- `top_retain` -- prevent specific toplevel functions and variables from `unused` + removal (can be array, comma-separated, RegExp or function. Implies `toplevel`) - `hoist_funs` -- hoist function declarations diff --git a/lib/compress.js b/lib/compress.js index 459256f5..0dcfb2ba 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -60,6 +60,8 @@ function Compressor(options, false_by_default) { booleans : !false_by_default, loops : !false_by_default, unused : !false_by_default, + toplevel : !!options["top_retain"], + top_retain : null, hoist_funs : !false_by_default, keep_fargs : true, keep_fnames : false, @@ -80,6 +82,21 @@ function Compressor(options, false_by_default) { global_defs : {}, passes : 1, }, true); + var top_retain = this.options["top_retain"]; + if (top_retain instanceof RegExp) { + this.top_retain = function(def) { + return top_retain.test(def.name); + }; + } else if (typeof top_retain === "function") { + this.top_retain = top_retain; + } else if (top_retain) { + if (typeof top_retain === "string") { + top_retain = top_retain.split(/,/); + } + this.top_retain = function(def) { + return top_retain.indexOf(def.name) >= 0; + }; + } var sequences = this.options["sequences"]; this.sequences_limit = sequences == 1 ? 200 : sequences | 0; this.warnings_produced = {}; @@ -1409,13 +1426,27 @@ merge(Compressor.prototype, { AST_Scope.DEFMETHOD("drop_unused", function(compressor){ var self = this; if (compressor.has_directive("use asm")) return self; + var toplevel = compressor.option("toplevel"); if (compressor.option("unused") - && !(self instanceof AST_Toplevel) + && (!(self instanceof AST_Toplevel) || toplevel) && !self.uses_eval - && !self.uses_with - ) { + && !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 = {}; // 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 initializations = new Dictionary(); // pass 1: find out which symbols are directly used in // this scope (not in nested scopes). @@ -1423,11 +1454,25 @@ merge(Compressor.prototype, { 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); + } + } 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){ + if (!drop_vars) { + var node_def = def.name.definition(); + 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)) { @@ -1437,6 +1482,14 @@ merge(Compressor.prototype, { }); 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)) { @@ -1496,7 +1549,7 @@ merge(Compressor.prototype, { } } } - if (node instanceof AST_Defun && node !== self) { + if (drop_funcs && node instanceof AST_Defun && node !== self) { if (!(node.name.definition().id in in_use_ids)) { compressor.warn("Dropping unused function {name} [{file}:{line},{col}]", { name : node.name.name, @@ -1508,7 +1561,7 @@ merge(Compressor.prototype, { } return node; } - if (node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { + if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { var def = node.definitions.filter(function(def){ if (def.name.definition().id in in_use_ids) return true; var w = { @@ -1571,6 +1624,15 @@ merge(Compressor.prototype, { } 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 node.right; + } + } if (node instanceof AST_For) { descend(node, this); diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index ef7af9ed..d7432f3f 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -338,8 +338,9 @@ collapse_vars_while: { collapse_vars_do_while: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, - comparisons:true, evaluate:true, booleans:false, loops:false, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + comparisons:true, evaluate:true, booleans:false, loops:false, unused:"keep_assign", + hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, cascade:true, + side_effects:true } input: { function f1(y) { @@ -409,6 +410,79 @@ collapse_vars_do_while: { } } +collapse_vars_do_while_drop_assign: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:false, loops:false, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1(y) { + // The constant do-while condition `c` will be replaced. + var c = 9; + do { } while (c === 77); + } + function f2(y) { + // The non-constant do-while condition `c` will not be replaced. + var c = 5 - y; + do { } while (c); + } + function f3(y) { + // The constant `x` will be replaced in the do loop body. + function fn(n) { console.log(n); } + var a = 2, x = 7; + do { + fn(a = x); + break; + } while (y); + } + function f4(y) { + // The non-constant `a` will not be replaced in the do loop body. + var a = y / 4; + do { + return a; + } while (y); + } + function f5(y) { + function p(x) { console.log(x); } + do { + // The non-constant `a` will be replaced in p(a) + // because it is declared in same block. + var a = y - 3; + p(a); + } while (--y); + } + } + expect: { + function f1(y) { + do ; while (false); + } + function f2(y) { + var c = 5 - y; + do ; while (c); + } + function f3(y) { + function fn(n) { console.log(n); } + do { + fn(7); + break; + } while (y); + } + function f4(y) { + var a = y / 4; + do + return a; + while (y); + } + function f5(y) { + function p(x) { console.log(x); } + do { + p(y - 3); + } while (--y); + } + } +} + collapse_vars_seq: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, @@ -567,8 +641,9 @@ collapse_vars_assignment: { collapse_vars_lvalues: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, - comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + comparisons:true, evaluate:true, booleans:true, loops:true, unused:"keep_assign", + hoist_funs:true, keep_fargs:true, if_return:true, join_vars:true, cascade:true, + side_effects:true } input: { function f0(x) { var i = ++x; return x += i; } @@ -593,7 +668,38 @@ collapse_vars_lvalues: { function f7(x) { var w = e1(), v = e2(), c = v - x; return (w = x) - c; } function f8(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } function f9(x) { var w = e1(); return e2() - x - (w = x); } + } +} +collapse_vars_lvalues_drop_assign: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f0(x) { var i = ++x; return x += i; } + function f1(x) { var a = (x -= 3); return x += a; } + function f2(x) { var z = x, a = ++z; return z += a; } + function f3(x) { var a = (x -= 3), b = x + a; return b; } + function f4(x) { var a = (x -= 3); return x + a; } + function f5(x) { var w = e1(), v = e2(), c = v = --x, b = w = x; return b - c; } + function f6(x) { var w = e1(), v = e2(), c = v = --x, b = w = x; return c - b; } + function f7(x) { var w = e1(), v = e2(), c = v - x, b = w = x; return b - c; } + function f8(x) { var w = e1(), v = e2(), b = w = x, c = v - x; return b - c; } + function f9(x) { var w = e1(), v = e2(), b = w = x, c = v - x; return c - b; } + } + expect: { + function f0(x) { var i = ++x; return x += i; } + function f1(x) { var a = (x -= 3); return x += a; } + function f2(x) { var z = x, a = ++z; return z += a; } + function f3(x) { var a = (x -= 3); return x + a; } + function f4(x) { var a = (x -= 3); return x + a; } + function f5(x) { var v = (e1(), e2()), c = v = --x; return x - c; } + function f6(x) { e1(), e2(); return --x - x; } + function f7(x) { var v = (e1(), e2()), c = v - x; return x - c; } + function f8(x) { var v = (e1(), e2()); return x - (v - x); } + function f9(x) { e1(); return e2() - x - x; } } } diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 035a428e..5620cf40 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -177,3 +177,382 @@ keep_fnames: { } } } + +drop_assign: { + options = { unused: true }; + input: { + function f1() { + var a; + a = 1; + } + function f2() { + var a = 1; + a = 2; + } + function f3(a) { + a = 1; + } + function f4() { + var a; + return a = 1; + } + function f5() { + var a; + return function() { + a = 1; + } + } + } + expect: { + function f1() { + 1; + } + function f2() { + 2; + } + function f3(a) { + 1; + } + function f4() { + return 1; + } + function f5() { + var a; + return function() { + a = 1; + } + } + } +} + +keep_assign: { + options = { unused: "keep_assign" }; + input: { + function f1() { + var a; + a = 1; + } + function f2() { + var a = 1; + a = 2; + } + function f3(a) { + a = 1; + } + function f4() { + var a; + return a = 1; + } + function f5() { + var a; + return function() { + a = 1; + } + } + } + expect: { + function f1() { + var a; + a = 1; + } + function f2() { + var a = 1; + a = 2; + } + function f3(a) { + a = 1; + } + function f4() { + var a; + return a = 1; + } + function f5() { + var a; + return function() { + a = 1; + } + } + } +} + +drop_toplevel_funcs: { + options = { toplevel: "funcs", unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var a, b = 1, c = g; + a = 2; + function g() {} + console.log(b = 3); + } +} + +drop_toplevel_vars: { + options = { toplevel: "vars", unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var c = g; + function f(d) { + return function() { + c = 2; + } + } + 2; + function g() {} + function h() {} + console.log(3); + } +} + +drop_toplevel_vars_fargs: { + options = { keep_fargs: false, toplevel: "vars", unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var c = g; + function f() { + return function() { + c = 2; + } + } + 2; + function g() {} + function h() {} + console.log(3); + } +} + +drop_toplevel_all: { + options = { toplevel: true, unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + 2; + console.log(3); + } +} + +drop_toplevel_retain: { + options = { top_retain: "f,a,o", unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var a, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + console.log(3); + } +} + +drop_toplevel_retain_array: { + options = { top_retain: [ "f", "a", "o" ], unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var a, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + console.log(3); + } +} + +drop_toplevel_retain_regex: { + options = { top_retain: /^[fao]$/, unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var a, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + console.log(3); + } +} + +drop_toplevel_all_retain: { + options = { toplevel: true, top_retain: "f,a,o", unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var a, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + console.log(3); + } +} + +drop_toplevel_funcs_retain: { + options = { toplevel: "funcs", top_retain: "f,a,o", unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + console.log(b = 3); + } +} + +drop_toplevel_vars_retain: { + options = { toplevel: "vars", top_retain: "f,a,o", unused: true }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var a, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(3); + } +} + +drop_toplevel_keep_assign: { + options = { toplevel: true, unused: "keep_assign" }; + input: { + var a, b = 1, c = g; + function f(d) { + return function() { + c = 2; + } + } + a = 2; + function g() {} + function h() {} + console.log(b = 3); + } + expect: { + var a, b = 1; + a = 2; + console.log(b = 3); + } +} -- 2.34.1