From dd31d12a9110b34c1b45a72b6e1f2b64c2d7afe9 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 18 Feb 2017 18:56:18 +0800 Subject: [PATCH 01/28] Improve optimizing `function() { if(c){return foo} bar();}` closes #1437 --- lib/compress.js | 10 ++-- test/compress/if_return.js | 101 ++++++++++++++++++++++++++++++++++++- test/compress/issue-979.js | 2 +- test/mocha/cli.js | 2 +- 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 4e45df92..04aa1072 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -546,7 +546,7 @@ merge(Compressor.prototype, { var self = compressor.self(); var multiple_if_returns = has_multiple_if_returns(statements); var in_lambda = self instanceof AST_Lambda; - var ret = []; + var ret = []; // Optimized statements, build from tail to front loop: for (var i = statements.length; --i >= 0;) { var stat = statements[i]; switch (true) { @@ -607,19 +607,21 @@ merge(Compressor.prototype, { ret = funs.concat([ stat.transform(compressor) ]); continue loop; } + //--- - // XXX: what was the intention of this case? + // if (a) return b; if (c) return d; e; ==> return a ? b : c ? d : void e; + // // if sequences is not enabled, this can lead to an endless loop (issue #866). // however, with sequences on this helps producing slightly better output for // the example code. if (compressor.option("sequences") + && i > 0 && statements[i - 1] instanceof AST_If && statements[i - 1].body instanceof AST_Return && ret.length == 1 && in_lambda && ret[0] instanceof AST_SimpleStatement - && (!stat.alternative || stat.alternative instanceof AST_SimpleStatement)) { + && !stat.alternative) { CHANGED = true; ret.push(make_node(AST_Return, ret[0], { value: make_node(AST_Undefined, ret[0]) }).transform(compressor)); - ret = as_statement_array(stat.alternative).concat(ret); ret.unshift(stat); continue loop; } diff --git a/test/compress/if_return.js b/test/compress/if_return.js index 78a6e818..0ac45c3c 100644 --- a/test/compress/if_return.js +++ b/test/compress/if_return.js @@ -170,8 +170,51 @@ if_return_7: { } } expect: { - // suboptimal - function f(x){return!!x||(foo(),void bar())} + function f(x){if(x)return!0;foo(),bar()} + } +} + +if_return_8: { + options = { + if_return: true, + sequences: true, + conditionals: true, + side_effects : true, + } + input: { + function f(e) { + if (2 == e) return foo(); + if (3 == e) return bar(); + if (4 == e) return baz(); + fail(e); + } + + function g(e) { + if (a(e)) return foo(); + if (b(e)) return bar(); + if (c(e)) return baz(); + fail(e); + } + + function h(e) { + if (a(e)) return foo(); + else if (b(e)) return bar(); + else if (c(e)) return baz(); + else fail(e); + } + + function i(e) { + if (a(e)) return foo(); + else if (b(e)) return bar(); + else if (c(e)) return baz(); + fail(e); + } + } + expect: { + function f(e){return 2==e?foo():3==e?bar():4==e?baz():void fail(e)} + function g(e){return a(e)?foo():b(e)?bar():c(e)?baz():void fail(e)} + function h(e){return a(e)?foo():b(e)?bar():c(e)?baz():void fail(e)} + function i(e){return a(e)?foo():b(e)?bar():c(e)?baz():void fail(e)} } } @@ -205,3 +248,57 @@ issue_1089: { } } } + +issue_1437: { + options = { + if_return : true, + sequences : true, + conditionals : false + } + input: { + function x() { + if (a()) + return b(); + if (c()) + return d(); + else + e(); + f(); + } + } + expect: { + function x() { + if (a()) + return b(); + if (c()) + return d(); + else + e() + f(); + } + } +} + +issue_1437_conditionals: { + options = { + conditionals : true, + if_return : true, + sequences : true + } + input: { + function x() { + if (a()) + return b(); + if (c()) + return d(); + else + e(); + f(); + } + } + expect: { + function x() { + return a() ? b() : c() ? d() : (e(), f(), void 0); + } + } +} diff --git a/test/compress/issue-979.js b/test/compress/issue-979.js index bae15db8..7ed5801d 100644 --- a/test/compress/issue-979.js +++ b/test/compress/issue-979.js @@ -82,7 +82,7 @@ issue979_test_negated_is_best: { 1!=a||2!=b||foo(); } function f7() { - return 1!=a&&2!=b?bar():void foo(); + if(1!=a&&2!=b)return bar();foo() } } } diff --git a/test/mocha/cli.js b/test/mocha/cli.js index a8de05c5..c5b571bd 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -4,7 +4,7 @@ var exec = require("child_process").exec; describe("bin/uglifyjs", function () { var uglifyjscmd = '"' + process.argv[0] + '" bin/uglifyjs'; it("should produce a functional build when using --self", function (done) { - this.timeout(5000); + this.timeout(15000); var command = uglifyjscmd + ' --self -cm --wrap WrappedUglifyJS'; From 11676f9d72e667ea14ce380ed448da948e79f85e Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 18:58:23 +0800 Subject: [PATCH 02/28] fix crash in unsafe replacement of undefined remove extraneous call to AST_SymbolRef.reference() closes #1443 --- lib/compress.js | 4 +-- test/compress/issue-1443.js | 69 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 test/compress/issue-1443.js diff --git a/lib/compress.js b/lib/compress.js index 04aa1072..a15206e8 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2721,13 +2721,11 @@ merge(Compressor.prototype, { var scope = compressor.find_parent(AST_Scope); var undef = scope.find_variable("undefined"); if (undef) { - var ref = make_node(AST_SymbolRef, self, { + return make_node(AST_SymbolRef, self, { name : "undefined", scope : scope, thedef : undef }); - ref.reference(); - return ref; } } return self; diff --git a/test/compress/issue-1443.js b/test/compress/issue-1443.js new file mode 100644 index 00000000..a2565872 --- /dev/null +++ b/test/compress/issue-1443.js @@ -0,0 +1,69 @@ +// tests assume that variable `undefined` not redefined and has `void 0` as value + +unsafe_undefined: { + options = { + if_return: true, + unsafe: true + } + mangle = {} + input: { + function f(undefined) { + return function() { + if (a) + return b; + if (c) + return d; + }; + } + } + expect: { + function f(n) { + return function() { + if (a) + return b; + if (c) + return d; + else + return n; + }; + } + } +} + +keep_fnames: { + options = { + if_return: true, + unsafe: true + } + mangle = { + keep_fnames: true + } + input: { + function f(undefined) { + return function() { + function n(a) { + return a * a; + } + if (a) + return b; + if (c) + return d; + }; + } + } + expect: { + function f(r) { + return function() { + function n(n) { + return n * n; + } + if (a) + return b; + if (c) + return d; + else + return r; + }; + } + } +} From 686a496b1c8c3762768c0da6ef98b37167d4d4ba Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 18:59:40 +0800 Subject: [PATCH 03/28] remove unused AST_Scope.nesting & AST_SymbolRef.frame they are computed but never used closes #1444 --- lib/scope.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 55d1eff1..0fe8d83a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -98,25 +98,24 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var labels = new Dictionary(); var defun = null; var last_var_had_const_pragma = false; - var nesting = 0; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; scope = new AST_Scope(node); - scope.init_scope_vars(nesting); + scope.init_scope_vars(); scope.parent_scope = save_scope; descend(); scope = save_scope; return true; } if (node instanceof AST_Scope) { - node.init_scope_vars(nesting); + node.init_scope_vars(); var save_scope = node.parent_scope = scope; var save_defun = defun; var save_labels = labels; defun = scope = node; labels = new Dictionary(); - ++nesting; descend(); --nesting; + descend(); scope = save_scope; defun = save_defun; labels = save_labels; @@ -244,7 +243,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } }); -AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ +AST_Scope.DEFMETHOD("init_scope_vars", function(){ this.variables = new Dictionary(); // map name to AST_SymbolVar (variables defined in this scope; includes functions) this.functions = new Dictionary(); // map name to AST_SymbolDefun (functions defined in this scope) this.uses_with = false; // will be set to true if this or some nested scope uses the `with` statement @@ -252,7 +251,6 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ this.parent_scope = null; // the parent scope this.enclosed = []; // a list of variables from this or outer scope(s) that are referenced from this or inner scopes this.cname = -1; // the current index for mangling functions/variables - this.nesting = nesting; // the nesting level of this scope (0 means toplevel) }); AST_Lambda.DEFMETHOD("init_scope_vars", function(){ @@ -278,7 +276,6 @@ AST_SymbolRef.DEFMETHOD("reference", function(options) { } s = s.parent_scope; } - this.frame = this.scope.nesting - def.scope.nesting; }); AST_Scope.DEFMETHOD("find_variable", function(name){ From fa668a28b47e06d838659d4e0910460c84ca3a61 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:00:54 +0800 Subject: [PATCH 04/28] fix corner case in keep_fnames happens when inner function: - just below top level - not referenced - `unused` is disabled closes #1445 --- lib/scope.js | 4 ++-- test/compress/issue-1431.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 0fe8d83a..d5cadd34 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -268,12 +268,12 @@ AST_SymbolRef.DEFMETHOD("reference", function(options) { var s = this.scope; while (s) { push_uniq(s.enclosed, def); - if (s === def.scope) break; if (options.keep_fnames) { - s.variables.each(function(d) { + s.functions.each(function(d) { push_uniq(def.scope.enclosed, d); }); } + if (s === def.scope) break; s = s.parent_scope; } }); diff --git a/test/compress/issue-1431.js b/test/compress/issue-1431.js index 731ebba8..9493fd37 100644 --- a/test/compress/issue-1431.js +++ b/test/compress/issue-1431.js @@ -1,3 +1,32 @@ +level_zero: { + options = { + keep_fnames: true + } + mangle = { + keep_fnames: true + } + input: { + function f(x) { + function n(a) { + return a * a; + } + return function() { + return x; + }; + } + } + expect: { + function f(r) { + function n(n) { + return n * n; + } + return function() { + return r; + }; + } + } +} + level_one: { options = { keep_fnames: true From e5badb954157d41dba3cc74f8813a90a145d9ca3 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:01:42 +0800 Subject: [PATCH 05/28] enable typeof "undefined" for general use move out of unsafe, guard corner case with screw_id8 instead closes #1446 --- lib/compress.js | 11 +++--- test/compress/issue-105.js | 25 ------------- test/compress/issue-1446.js | 71 +++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 30 deletions(-) delete mode 100644 test/compress/issue-105.js create mode 100644 test/compress/issue-1446.js diff --git a/lib/compress.js b/lib/compress.js index a15206e8..b49ebef2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2504,14 +2504,15 @@ merge(Compressor.prototype, { // XXX: intentionally falling down to the next case case "==": case "!=": + // "undefined" == typeof x => undefined === x if (self.left instanceof AST_String && self.left.value == "undefined" && self.right instanceof AST_UnaryPrefix - && self.right.operator == "typeof" - && compressor.option("unsafe")) { - if (!(self.right.expression instanceof AST_SymbolRef) - || !self.right.expression.undeclared()) { - self.right = self.right.expression; + && self.right.operator == "typeof") { + var expr = self.right.expression; + if (expr instanceof AST_SymbolRef ? !expr.undeclared() + : !(expr instanceof AST_PropAccess) || compressor.option("screw_ie8")) { + self.right = expr; self.left = make_node(AST_Undefined, self.left).optimize(compressor); if (self.operator.length == 2) self.operator += "="; } diff --git a/test/compress/issue-105.js b/test/compress/issue-105.js deleted file mode 100644 index ca17adbf..00000000 --- a/test/compress/issue-105.js +++ /dev/null @@ -1,25 +0,0 @@ -typeof_eq_undefined: { - options = { - comparisons: true - }; - input: { a = typeof b.c != "undefined" } - expect: { a = "undefined" != typeof b.c } -} - -typeof_eq_undefined_unsafe: { - options = { - comparisons: true, - unsafe: true - }; - input: { a = typeof b.c != "undefined" } - expect: { a = void 0 !== b.c } -} - -typeof_eq_undefined_unsafe2: { - options = { - comparisons: true, - unsafe: true - }; - input: { a = "undefined" != typeof b.c } - expect: { a = void 0 !== b.c } -} diff --git a/test/compress/issue-1446.js b/test/compress/issue-1446.js new file mode 100644 index 00000000..3d69aa09 --- /dev/null +++ b/test/compress/issue-1446.js @@ -0,0 +1,71 @@ +typeof_eq_undefined: { + options = { + comparisons: true + } + input: { + var a = typeof b != "undefined"; + b = typeof a != "undefined"; + var c = typeof d.e !== "undefined"; + var f = "undefined" === typeof g; + g = "undefined" === typeof f; + var h = "undefined" == typeof i.j; + } + expect: { + var a = "undefined" != typeof b; + b = void 0 !== a; + var c = void 0 !== d.e; + var f = "undefined" == typeof g; + g = void 0 === f; + var h = void 0 === i.j; + } +} + +typeof_eq_undefined_ie8: { + options = { + comparisons: true, + screw_ie8: false + } + input: { + var a = typeof b != "undefined"; + b = typeof a != "undefined"; + var c = typeof d.e !== "undefined"; + var f = "undefined" === typeof g; + g = "undefined" === typeof f; + var h = "undefined" == typeof i.j; + } + expect: { + var a = "undefined" != typeof b; + b = void 0 !== a; + var c = "undefined" != typeof d.e; + var f = "undefined" == typeof g; + g = void 0 === f; + var h = "undefined" == typeof i.j; + } +} + +undefined_redefined: { + options = { + comparisons: true + } + input: { + function f(undefined) { + var n = 1; + return typeof n == "undefined"; + } + } + expect_exact: "function f(undefined){var n=1;return void 0===n}" +} + +undefined_redefined_mangle: { + options = { + comparisons: true + } + mangle = {} + input: { + function f(undefined) { + var n = 1; + return typeof n == "undefined"; + } + } + expect_exact: "function f(n){var r=1;return void 0===r}" +} From d11dca3cf9e34302ce12a6c9f1cd81b22551f2ba Mon Sep 17 00:00:00 2001 From: kzc Date: Sat, 18 Feb 2017 19:02:59 +0800 Subject: [PATCH 06/28] fix stray else in compress with conditionals=false closes #1449 --- lib/compress.js | 3 ++- test/compress/issue-1447.js | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1447.js diff --git a/lib/compress.js b/lib/compress.js index b49ebef2..459256f5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1840,6 +1840,8 @@ merge(Compressor.prototype, { }); OPT(AST_If, function(self, compressor){ + if (is_empty(self.alternative)) self.alternative = null; + if (!compressor.option("conditionals")) return self; // if condition can be statically determined, warn and drop // one of the blocks. note, statically determined implies @@ -1868,7 +1870,6 @@ merge(Compressor.prototype, { } } } - if (is_empty(self.alternative)) self.alternative = null; var negated = self.condition.negate(compressor); var self_condition_length = self.condition.print_to_string().length; var negated_length = negated.print_to_string().length; diff --git a/test/compress/issue-1447.js b/test/compress/issue-1447.js new file mode 100644 index 00000000..163acbc2 --- /dev/null +++ b/test/compress/issue-1447.js @@ -0,0 +1,45 @@ +else_with_empty_block: { + options = {} + input: { + if (x) + yes(); + else { + } + } + expect_exact: "if(x)yes();" +} + +else_with_empty_statement: { + options = {} + input: { + if (x) + yes(); + else + ; + } + expect_exact: "if(x)yes();" +} + +conditional_false_stray_else_in_loop: { + options = { + evaluate : true, + comparisons : true, + booleans : true, + unused : true, + loops : true, + side_effects : true, + dead_code : true, + hoist_vars : true, + join_vars : true, + if_return : true, + cascade : true, + conditionals : false, + } + input: { + for (var i = 1; i <= 4; ++i) { + if (i <= 2) continue; + console.log(i); + } + } + expect_exact: "for(var i=1;i<=4;++i)if(!(i<=2))console.log(i);" +} From 148047fbbf1951a52e69170edf510c59b3899e6c Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:03:53 +0800 Subject: [PATCH 07/28] 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); + } +} From 100307ab31e89075a5b0e56d47597a0525dd43a6 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:05:11 +0800 Subject: [PATCH 08/28] fixes & improvements to [].join() fixes - [a].join() => "" + a - ["a", , "b"].join() => "a,,b" - ["a", null, "b"].join() => "a,,b" - ["a", undefined, "b"].join() => "a,,b" improvements - ["a", "b"].join(null) => "anullb" - ["a", "b"].join(undefined) => "a,b" - [a + "b", c].join("") => a + "b" + c closes #1453 --- lib/compress.js | 62 ++++++++++++++++++++++++++--------------- test/compress/arrays.js | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 0dcfb2ba..2ba2982e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2290,39 +2290,57 @@ merge(Compressor.prototype, { }).transform(compressor); } else if (exp instanceof AST_Dot && exp.expression instanceof AST_Array && exp.property == "join") EXIT: { - var separator = self.args.length == 0 ? "," : self.args[0].evaluate(compressor)[1]; - if (separator == null) break EXIT; // not a constant - var elements = exp.expression.elements.reduce(function(a, el){ + var separator; + if (self.args.length > 0) { + separator = self.args[0].evaluate(compressor); + if (separator.length < 2) break EXIT; // not a constant + separator = separator[1]; + } + var elements = []; + var consts = []; + exp.expression.elements.forEach(function(el) { el = el.evaluate(compressor); - if (a.length == 0 || el.length == 1) { - a.push(el); + if (el.length > 1) { + consts.push(el[1]); } else { - var last = a[a.length - 1]; - if (last.length == 2) { - // it's a constant - var val = "" + last[1] + separator + el[1]; - a[a.length - 1] = [ make_node_from_constant(compressor, val, last[0]), val ]; - } else { - a.push(el); + if (consts.length > 0) { + elements.push(make_node(AST_String, self, { + value: consts.join(separator) + })); + consts.length = 0; } + elements.push(el[0]); } - return a; - }, []); + }); + if (consts.length > 0) { + elements.push(make_node(AST_String, self, { + value: consts.join(separator) + })); + } if (elements.length == 0) return make_node(AST_String, self, { value: "" }); - if (elements.length == 1) return elements[0][0]; + if (elements.length == 1) { + if (elements[0].is_string(compressor)) { + return elements[0]; + } + return make_node(AST_Binary, elements[0], { + operator : "+", + left : make_node(AST_String, self, { value: "" }), + right : elements[0] + }); + } if (separator == "") { var first; - if (elements[0][0] instanceof AST_String - || elements[1][0] instanceof AST_String) { - first = elements.shift()[0]; + if (elements[0].is_string(compressor) + || elements[1].is_string(compressor)) { + first = elements.shift(); } else { first = make_node(AST_String, self, { value: "" }); } return elements.reduce(function(prev, el){ - return make_node(AST_Binary, el[0], { + return make_node(AST_Binary, el, { operator : "+", left : prev, - right : el[0], + right : el }); }, first).transform(compressor); } @@ -2331,9 +2349,7 @@ merge(Compressor.prototype, { var node = self.clone(); node.expression = node.expression.clone(); node.expression.expression = node.expression.expression.clone(); - node.expression.expression.elements = elements.map(function(el){ - return el[0]; - }); + node.expression.expression.elements = elements; return best_of(self, node); } } diff --git a/test/compress/arrays.js b/test/compress/arrays.js index 77ef761a..2e1f86ed 100644 --- a/test/compress/arrays.js +++ b/test/compress/arrays.js @@ -21,10 +21,19 @@ constant_join: { input: { var a = [ "foo", "bar", "baz" ].join(""); var a1 = [ "foo", "bar", "baz" ].join(); + var a2 = [ "foo", "bar", "baz" ].join(null); + var a3 = [ "foo", "bar", "baz" ].join(void 0); + var a4 = [ "foo", , "baz" ].join(); + var a5 = [ "foo", null, "baz" ].join(); + var a6 = [ "foo", void 0, "baz" ].join(); var b = [ "foo", 1, 2, 3, "bar" ].join(""); var c = [ boo(), "foo", 1, 2, 3, "bar", bar() ].join(""); var c1 = [ boo(), bar(), "foo", 1, 2, 3, "bar", bar() ].join(""); var c2 = [ 1, 2, "foo", "bar", baz() ].join(""); + var c3 = [ boo() + bar() + "foo", 1, 2, 3, "bar", bar() + "foo" ].join(""); + var c4 = [ 1, 2, null, undefined, "foo", "bar", baz() ].join(""); + var c5 = [ boo() + bar() + "foo", 1, 2, 3, "bar", bar() + "foo" ].join(); + var c6 = [ 1, 2, null, undefined, "foo", "bar", baz() ].join(); var d = [ "foo", 1 + 2 + "bar", "baz" ].join("-"); var e = [].join(foo + bar); var f = [].join(""); @@ -33,10 +42,19 @@ constant_join: { expect: { var a = "foobarbaz"; var a1 = "foo,bar,baz"; + var a2 = "foonullbarnullbaz"; + var a3 = "foo,bar,baz"; + var a4 = "foo,,baz"; + var a5 = "foo,,baz"; + var a6 = "foo,,baz"; var b = "foo123bar"; var c = boo() + "foo123bar" + bar(); var c1 = "" + boo() + bar() + "foo123bar" + bar(); var c2 = "12foobar" + baz(); + var c3 = boo() + bar() + "foo123bar" + (bar() + "foo"); + var c4 = "12foobar" + baz(); + var c5 = [ boo() + bar() + "foo", 1, 2, 3, "bar", bar() + "foo" ].join(); + var c6 = [ "1,2,,,foo,bar", baz() ].join(); var d = "foo-3bar-baz"; var e = [].join(foo + bar); var f = ""; @@ -73,6 +91,41 @@ constant_join_2: { } } +constant_join_3: { + options = { + unsafe: true, + evaluate: true, + }; + input: { + var a = [ null ].join(); + var b = [ , ].join(); + var c = [ , 1, , 3 ].join(); + var d = [ foo ].join(); + var e = [ foo, null, undefined, bar ].join("-"); + var f = [ foo, bar ].join(""); + var g = [ null, "foo", null, bar + "baz" ].join(""); + var h = [ null, "foo", null, bar + "baz" ].join("-"); + var i = [ "foo" + bar, null, baz + "moo" ].join(""); + var j = [ foo + "bar", baz ].join(""); + var k = [ foo, "bar" + baz ].join(""); + var l = [ foo, bar + "baz" ].join(""); + } + expect: { + var a = ""; + var b = ""; + var c = ",1,,3"; + var d = "" + foo; + var e = [ foo, "-", bar ].join("-"); + var f = "" + foo + bar; + var g = "foo" + (bar + "baz"); + var h = [ "-foo-", bar + "baz" ].join("-"); + var i = "foo" + bar + (baz + "moo"); + var j = foo + "bar" + baz; + var k = foo + ("bar" + baz); + var l = foo + (bar + "baz"); + } +} + for_loop: { options = { unsafe : true, From ae4db00991c6155fde42bd00c30614d922a4219a Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:05:54 +0800 Subject: [PATCH 09/28] tweak do-while loops - `do{...}while(false)` => `{...}` - clean up `AST_While` logic closes #1452 --- lib/compress.js | 16 ++++++---------- test/compress/loops.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 2ba2982e..ee28ee14 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1813,8 +1813,14 @@ merge(Compressor.prototype, { extract_declarations_from_unreachable_code(compressor, self.body, a); return make_node(AST_BlockStatement, self, { body: a }); } + } else { + // self instanceof AST_Do + return self.body; } } + if (self instanceof AST_While) { + return make_node(AST_For, self, self).transform(compressor); + } return self; }); @@ -1863,16 +1869,6 @@ merge(Compressor.prototype, { } }; - OPT(AST_While, function(self, compressor) { - if (!compressor.option("loops")) return self; - self = AST_DWLoop.prototype.optimize.call(self, compressor); - if (self instanceof AST_While) { - if_break_in_loop(self, compressor); - self = make_node(AST_For, self, self).transform(compressor); - } - return self; - }); - OPT(AST_For, function(self, compressor){ var cond = self.condition; if (cond) { diff --git a/test/compress/loops.js b/test/compress/loops.js index 78f618aa..ca05461c 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -187,3 +187,32 @@ keep_collapse_const_in_own_block_scope_2: { console.log(c); } } + +evaluate: { + options = { + loops: true, + dead_code: true, + evaluate: true, + }; + input: { + while (true) { + a(); + } + while (false) { + b(); + } + do { + c(); + } while (true); + do { + d(); + } while (false); + } + expect: { + for(;;) + a(); + for(;;) + c(); + d(); + } +} From f584ca8d0766fb6d2a254dd4487afa91ed2c5034 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:07:03 +0800 Subject: [PATCH 10/28] `-c sequences=N` suboptimal at N expression cutoff N = 2: a; b; c; d; was: a, b; c; d; now: a, b; c, d; fixes #1455 closes #1457 --- lib/compress.js | 5 ++++- test/compress/sequences.js | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index ee28ee14..e8b271f3 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -749,7 +749,10 @@ merge(Compressor.prototype, { seq = []; }; statements.forEach(function(stat){ - if (stat instanceof AST_SimpleStatement && seqLength(seq) < compressor.sequences_limit) { + if (stat instanceof AST_SimpleStatement) { + if (seqLength(seq) >= compressor.sequences_limit) { + push_seq(); + } seq.push(stat.body); } else { push_seq(); diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 0e3319ab..8a3ffe89 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -169,3 +169,47 @@ for_sequences: { for (y = 5; false;); } } + +limit_1: { + options = { + sequences: 3, + }; + input: { + a; + b; + c; + d; + e; + f; + g; + h; + i; + j; + k; + } + expect: { + a, b, c; + d, e, f; + g, h, i; + j, k; + } +} + +limit_2: { + options = { + sequences: 3, + }; + input: { + a, b; + c, d; + e, f; + g, h; + i, j; + k; + } + expect: { + a, b, c, d; + e, f, g, h; + i, j, k; + } +} From 6b3c49e45837e8e1b32b60fe3b217b965ac16efd Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:07:52 +0800 Subject: [PATCH 11/28] improve string concatenation shuffle associative operations to minimise parentheses and aid other uglification efforts closes #1454 --- lib/compress.js | 9 +- test/compress/arrays.js | 8 +- test/compress/concat-strings.js | 140 ++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 5 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index e8b271f3..536b7518 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2751,9 +2751,16 @@ merge(Compressor.prototype, { } // x && (y && z) ==> x && y && z // x || (y || z) ==> x || y || z + // x + ("y" + z) ==> x + "y" + z + // "x" + (y + "z")==> "x" + y + "z" if (self.right instanceof AST_Binary && self.right.operator == self.operator - && (self.operator == "&&" || self.operator == "||")) + && (self.operator == "&&" + || self.operator == "||" + || (self.operator == "+" + && (self.right.left.is_string(compressor) + || (self.left.is_string(compressor) + && self.right.right.is_string(compressor)))))) { self.left = make_node(AST_Binary, self.left, { operator : self.operator, diff --git a/test/compress/arrays.js b/test/compress/arrays.js index 2e1f86ed..f0ded06c 100644 --- a/test/compress/arrays.js +++ b/test/compress/arrays.js @@ -51,7 +51,7 @@ constant_join: { var c = boo() + "foo123bar" + bar(); var c1 = "" + boo() + bar() + "foo123bar" + bar(); var c2 = "12foobar" + baz(); - var c3 = boo() + bar() + "foo123bar" + (bar() + "foo"); + var c3 = boo() + bar() + "foo123bar" + bar() + "foo"; var c4 = "12foobar" + baz(); var c5 = [ boo() + bar() + "foo", 1, 2, 3, "bar", bar() + "foo" ].join(); var c6 = [ "1,2,,,foo,bar", baz() ].join(); @@ -117,11 +117,11 @@ constant_join_3: { var d = "" + foo; var e = [ foo, "-", bar ].join("-"); var f = "" + foo + bar; - var g = "foo" + (bar + "baz"); + var g = "foo" + bar + "baz"; var h = [ "-foo-", bar + "baz" ].join("-"); - var i = "foo" + bar + (baz + "moo"); + var i = "foo" + bar + baz + "moo"; var j = foo + "bar" + baz; - var k = foo + ("bar" + baz); + var k = foo + "bar" + baz; var l = foo + (bar + "baz"); } } diff --git a/test/compress/concat-strings.js b/test/compress/concat-strings.js index 50eef8b8..d2503c6d 100644 --- a/test/compress/concat-strings.js +++ b/test/compress/concat-strings.js @@ -24,3 +24,143 @@ concat_1: { var f = "\x00360\08\0"; } } + +concat_2: { + options = {}; + input: { + console.log( + 1 + (2 + 3), + 1 + (2 + "3"), + 1 + ("2" + 3), + 1 + ("2" + "3"), + "1" + (2 + 3), + "1" + (2 + "3"), + "1" + ("2" + 3), + "1" + ("2" + "3") + ); + } + expect: { + console.log( + 1 + (2 + 3), + 1 + (2 + "3"), + 1 + "2" + 3, + 1 + "2" + "3", + "1" + (2 + 3), + "1" + 2 + "3", + "1" + "2" + 3, + "1" + "2" + "3" + ); + } +} + +concat_3: { + options = {}; + input: { + console.log( + 1 + 2 + (3 + 4 + 5), + 1 + 2 + (3 + 4 + "5"), + 1 + 2 + (3 + "4" + 5), + 1 + 2 + (3 + "4" + "5"), + 1 + 2 + ("3" + 4 + 5), + 1 + 2 + ("3" + 4 + "5"), + 1 + 2 + ("3" + "4" + 5), + 1 + 2 + ("3" + "4" + "5") + ); + } + expect: { + console.log( + 1 + 2 + (3 + 4 + 5), + 1 + 2 + (3 + 4 + "5"), + 1 + 2 + (3 + "4") + 5, + 1 + 2 + (3 + "4") + "5", + 1 + 2 + "3" + 4 + 5, + 1 + 2 + "3" + 4 + "5", + 1 + 2 + "3" + "4" + 5, + 1 + 2 + "3" + "4" + "5" + ); + } +} + +concat_4: { + options = {}; + input: { + console.log( + 1 + "2" + (3 + 4 + 5), + 1 + "2" + (3 + 4 + "5"), + 1 + "2" + (3 + "4" + 5), + 1 + "2" + (3 + "4" + "5"), + 1 + "2" + ("3" + 4 + 5), + 1 + "2" + ("3" + 4 + "5"), + 1 + "2" + ("3" + "4" + 5), + 1 + "2" + ("3" + "4" + "5") + ); + } + expect: { + console.log( + 1 + "2" + (3 + 4 + 5), + 1 + "2" + (3 + 4) + "5", + 1 + "2" + 3 + "4" + 5, + 1 + "2" + 3 + "4" + "5", + 1 + "2" + "3" + 4 + 5, + 1 + "2" + "3" + 4 + "5", + 1 + "2" + "3" + "4" + 5, + 1 + "2" + "3" + "4" + "5" + ); + } +} + +concat_5: { + options = {}; + input: { + console.log( + "1" + 2 + (3 + 4 + 5), + "1" + 2 + (3 + 4 + "5"), + "1" + 2 + (3 + "4" + 5), + "1" + 2 + (3 + "4" + "5"), + "1" + 2 + ("3" + 4 + 5), + "1" + 2 + ("3" + 4 + "5"), + "1" + 2 + ("3" + "4" + 5), + "1" + 2 + ("3" + "4" + "5") + ); + } + expect: { + console.log( + "1" + 2 + (3 + 4 + 5), + "1" + 2 + (3 + 4) + "5", + "1" + 2 + 3 + "4" + 5, + "1" + 2 + 3 + "4" + "5", + "1" + 2 + "3" + 4 + 5, + "1" + 2 + "3" + 4 + "5", + "1" + 2 + "3" + "4" + 5, + "1" + 2 + "3" + "4" + "5" + ); + } +} + +concat_6: { + options = {}; + input: { + console.log( + "1" + "2" + (3 + 4 + 5), + "1" + "2" + (3 + 4 + "5"), + "1" + "2" + (3 + "4" + 5), + "1" + "2" + (3 + "4" + "5"), + "1" + "2" + ("3" + 4 + 5), + "1" + "2" + ("3" + 4 + "5"), + "1" + "2" + ("3" + "4" + 5), + "1" + "2" + ("3" + "4" + "5") + ); + } + expect: { + console.log( + "1" + "2" + (3 + 4 + 5), + "1" + "2" + (3 + 4) + "5", + "1" + "2" + 3 + "4" + 5, + "1" + "2" + 3 + "4" + "5", + "1" + "2" + "3" + 4 + 5, + "1" + "2" + "3" + 4 + "5", + "1" + "2" + "3" + "4" + 5, + "1" + "2" + "3" + "4" + "5" + ); + } +} From f0ff6189be3a75cd4ccb1c38051ec27f9b30d67f Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:11:57 +0800 Subject: [PATCH 12/28] clean up `negate_iife` - remove extra tree scanning phase for `negate_iife` - `negate_iife` now only deals with the narrowest form, i.e. IIFE sitting directly under `AST_SimpleStatement` - `booleans`, `conditionals` etc. will now take care the rest via more accurate accounting - `a(); void b();` => `a(); b();` fixes #1288 closes #1451 --- lib/compress.js | 142 ++++++++++++++++----------------- lib/output.js | 25 ------ lib/utils.js | 23 ++++++ test/compress/negate-iife.js | 148 +++++++++++++++++++++++++++++++++++ test/compress/sequences.js | 38 +++++++++ 5 files changed, 277 insertions(+), 99 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 536b7518..b66c5582 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -278,6 +278,15 @@ merge(Compressor.prototype, { return x; }; + var readOnlyPrefix = makePredicate("! ~ + - void typeof"); + function statement_to_expression(stat) { + if (stat.body instanceof AST_UnaryPrefix && readOnlyPrefix(stat.body.operator)) { + return stat.body.expression; + } else { + return stat.body; + } + } + function tighten_body(statements, compressor) { var CHANGED, max_iter = 10; do { @@ -303,10 +312,6 @@ merge(Compressor.prototype, { } } while (CHANGED && max_iter-- > 0); - if (compressor.option("negate_iife")) { - negate_iifes(statements, compressor); - } - return statements; function collapse_single_use_vars(statements, compressor) { @@ -753,7 +758,7 @@ merge(Compressor.prototype, { if (seqLength(seq) >= compressor.sequences_limit) { push_seq(); } - seq.push(stat.body); + seq.push(seq.length > 0 ? statement_to_expression(stat) : stat.body); } else { push_seq(); ret.push(stat); @@ -802,7 +807,7 @@ merge(Compressor.prototype, { stat.init = cons_seq(stat.init); } else if (!stat.init) { - stat.init = prev.body; + stat.init = statement_to_expression(prev); ret.pop(); } } catch(ex) { @@ -859,50 +864,6 @@ merge(Compressor.prototype, { }, []); }; - function negate_iifes(statements, compressor) { - function is_iife_call(node) { - if (node instanceof AST_Call) { - return node.expression instanceof AST_Function || is_iife_call(node.expression); - } - return false; - } - - statements.forEach(function(stat){ - if (stat instanceof AST_SimpleStatement) { - stat.body = (function transform(thing) { - return thing.transform(new TreeTransformer(function(node){ - if (node instanceof AST_New) { - return node; - } - if (is_iife_call(node)) { - return make_node(AST_UnaryPrefix, node, { - operator: "!", - expression: node - }); - } - else if (node instanceof AST_Call) { - node.expression = transform(node.expression); - } - else if (node instanceof AST_Seq) { - node.car = transform(node.car); - } - else if (node instanceof AST_Conditional) { - var expr = transform(node.condition); - if (expr !== node.condition) { - // it has been negated, reverse - node.condition = expr; - var tmp = node.consequent; - node.consequent = node.alternative; - node.alternative = tmp; - } - } - return node; - })); - })(stat.body); - } - }); - }; - }; function extract_functions_from_statement_array(statements) { @@ -1007,7 +968,15 @@ merge(Compressor.prototype, { return ast1.print_to_string().length > ast2.print_to_string().length ? ast2 : ast1; - }; + } + + function best_of_statement(ast1, ast2) { + return best_of(make_node(AST_SimpleStatement, ast1, { + body: ast1 + }), make_node(AST_SimpleStatement, ast2, { + body: ast2 + })).body; + } // methods to evaluate a constant expression (function (def){ @@ -1227,7 +1196,17 @@ merge(Compressor.prototype, { operator: "!", expression: exp }); - }; + } + function best(orig, alt, first_in_statement) { + var negated = basic_negation(orig); + if (first_in_statement) { + var stat = make_node(AST_SimpleStatement, alt, { + body: alt + }); + return best_of(negated, stat) === stat ? alt : negated; + } + return best_of(negated, alt); + } def(AST_Node, function(){ return basic_negation(this); }); @@ -1247,13 +1226,13 @@ merge(Compressor.prototype, { self.cdr = self.cdr.negate(compressor); return self; }); - def(AST_Conditional, function(compressor){ + def(AST_Conditional, function(compressor, first_in_statement){ var self = this.clone(); self.consequent = self.consequent.negate(compressor); self.alternative = self.alternative.negate(compressor); - return best_of(basic_negation(this), self); + return best(this, self, first_in_statement); }); - def(AST_Binary, function(compressor){ + def(AST_Binary, function(compressor, first_in_statement){ var self = this.clone(), op = this.operator; if (compressor.option("unsafe_comps")) { switch (op) { @@ -1270,20 +1249,20 @@ merge(Compressor.prototype, { case "!==": self.operator = "==="; return self; case "&&": self.operator = "||"; - self.left = self.left.negate(compressor); + self.left = self.left.negate(compressor, first_in_statement); self.right = self.right.negate(compressor); - return best_of(basic_negation(this), self); + return best(this, self, first_in_statement); case "||": self.operator = "&&"; - self.left = self.left.negate(compressor); + self.left = self.left.negate(compressor, first_in_statement); self.right = self.right.negate(compressor); - return best_of(basic_negation(this), self); + return best(this, self, first_in_statement); } return basic_negation(this); }); })(function(node, func){ - node.DEFMETHOD("negate", function(compressor){ - return func.call(this, compressor); + node.DEFMETHOD("negate", function(compressor, first_in_statement){ + return func.call(this, compressor, first_in_statement); }); }); @@ -1954,8 +1933,8 @@ merge(Compressor.prototype, { return make_node(AST_SimpleStatement, self, { body: make_node(AST_Conditional, self, { condition : self.condition, - consequent : self.body.body, - alternative : self.alternative.body + consequent : statement_to_expression(self.body), + alternative : statement_to_expression(self.alternative) }) }).transform(compressor); } @@ -1971,14 +1950,14 @@ merge(Compressor.prototype, { body: make_node(AST_Binary, self, { operator : "||", left : negated, - right : self.body.body + right : statement_to_expression(self.body) }) }).transform(compressor); return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "&&", left : self.condition, - right : self.body.body + right : statement_to_expression(self.body) }) }).transform(compressor); } @@ -1989,7 +1968,7 @@ merge(Compressor.prototype, { body: make_node(AST_Binary, self, { operator : "||", left : self.condition, - right : self.alternative.body + right : statement_to_expression(self.alternative) }) }).transform(compressor); } @@ -2372,7 +2351,19 @@ merge(Compressor.prototype, { } } } - return self.evaluate(compressor)[0]; + if (compressor.option("negate_iife") + && compressor.parent() instanceof AST_SimpleStatement + && is_iife_call(self)) { + return self.negate(compressor, true); + } + return self; + + function is_iife_call(node) { + if (node instanceof AST_Call && !(node instanceof AST_New)) { + return node.expression instanceof AST_Function || is_iife_call(node.expression); + } + return false; + } }); OPT(AST_New, function(self, compressor){ @@ -2459,6 +2450,10 @@ merge(Compressor.prototype, { // !!foo ==> foo, if we're in boolean context return e.expression; } + if (e instanceof AST_Binary) { + var statement = first_in_statement(compressor); + self = (statement ? best_of_statement : best_of)(self, e.negate(compressor, statement)); + } break; case "typeof": // typeof always returns a non-empty string, thus it's @@ -2472,9 +2467,6 @@ merge(Compressor.prototype, { } return make_node(AST_True, self); } - if (e instanceof AST_Binary && self.operator == "!") { - self = best_of(self, e.negate(compressor)); - } } return self.evaluate(compressor)[0]; }); @@ -2651,11 +2643,12 @@ merge(Compressor.prototype, { if (compressor.option("comparisons") && self.is_boolean()) { if (!(compressor.parent() instanceof AST_Binary) || compressor.parent() instanceof AST_Assign) { + var statement = first_in_statement(compressor); var negated = make_node(AST_UnaryPrefix, self, { operator: "!", - expression: self.negate(compressor) + expression: self.negate(compressor, statement) }); - self = best_of(self, negated); + self = (statement ? best_of_statement : best_of)(self, negated); } if (compressor.option("unsafe_comps")) { switch (self.operator) { @@ -2859,8 +2852,9 @@ merge(Compressor.prototype, { return maintain_this_binding(compressor.parent(), self, self.alternative); } } - var negated = cond[0].negate(compressor); - if (best_of(cond[0], negated) === negated) { + var statement = first_in_statement(compressor); + var negated = cond[0].negate(compressor, statement); + if ((statement ? best_of_statement : best_of)(cond[0], negated) === negated) { self = make_node(AST_Conditional, self, { condition: negated, consequent: self.alternative, diff --git a/lib/output.js b/lib/output.js index 50e5aa43..b6f0a873 100644 --- a/lib/output.js +++ b/lib/output.js @@ -425,7 +425,6 @@ function OutputStream(options) { pos : function() { return current_pos }, push_node : function(node) { stack.push(node) }, pop_node : function() { return stack.pop() }, - stack : function() { return stack }, parent : function(n) { return stack[stack.length - 2 - (n || 0)]; } @@ -1334,30 +1333,6 @@ function OutputStream(options) { } }; - // return true if the node at the top of the stack (that means the - // innermost node in the current output) is lexically the first in - // a statement. - function first_in_statement(output) { - var a = output.stack(), i = a.length, node = a[--i], p = a[--i]; - while (i > 0) { - if (p instanceof AST_Statement && p.body === node) - return true; - if ((p instanceof AST_Seq && p.car === node ) || - (p instanceof AST_Call && p.expression === node && !(p instanceof AST_New) ) || - (p instanceof AST_Dot && p.expression === node ) || - (p instanceof AST_Sub && p.expression === node ) || - (p instanceof AST_Conditional && p.condition === node ) || - (p instanceof AST_Binary && p.left === node ) || - (p instanceof AST_UnaryPostfix && p.expression === node )) - { - node = p; - p = a[--i]; - } else { - return false; - } - } - }; - // self should be AST_New. decide if we want to show parens or not. function need_constructor_parens(self, output) { // Always print parentheses with arguments diff --git a/lib/utils.js b/lib/utils.js index d0a21ac9..a0571d65 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -320,3 +320,26 @@ Dictionary.fromObject = function(obj) { function HOP(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } + +// return true if the node at the top of the stack (that means the +// innermost node in the current output) is lexically the first in +// a statement. +function first_in_statement(stack) { + var node = stack.parent(-1); + for (var i = 0, p; p = stack.parent(i); i++) { + if (p instanceof AST_Statement && p.body === node) + return true; + if ((p instanceof AST_Seq && p.car === node ) || + (p instanceof AST_Call && p.expression === node && !(p instanceof AST_New) ) || + (p instanceof AST_Dot && p.expression === node ) || + (p instanceof AST_Sub && p.expression === node ) || + (p instanceof AST_Conditional && p.condition === node ) || + (p instanceof AST_Binary && p.left === node ) || + (p instanceof AST_UnaryPostfix && p.expression === node )) + { + node = p; + } else { + return false; + } + } +} diff --git a/test/compress/negate-iife.js b/test/compress/negate-iife.js index 0c111604..312e0f28 100644 --- a/test/compress/negate-iife.js +++ b/test/compress/negate-iife.js @@ -10,6 +10,16 @@ negate_iife_1: { } } +negate_iife_1_off: { + options = { + negate_iife: false, + }; + input: { + (function(){ stuff() })(); + } + expect_exact: '(function(){stuff()})();' +} + negate_iife_2: { options = { negate_iife: true @@ -25,6 +35,20 @@ negate_iife_2: { negate_iife_3: { options = { negate_iife: true, + conditionals: true + }; + input: { + (function(){ return true })() ? console.log(true) : console.log(false); + } + expect: { + !function(){ return true }() ? console.log(false) : console.log(true); + } +} + +negate_iife_3_off: { + options = { + negate_iife: false, + conditionals: true, }; input: { (function(){ return true })() ? console.log(true) : console.log(false); @@ -37,6 +61,7 @@ negate_iife_3: { negate_iife_3: { options = { negate_iife: true, + conditionals: true, sequences: true }; input: { @@ -52,6 +77,41 @@ negate_iife_3: { } } +sequence_off: { + options = { + negate_iife: false, + conditionals: true, + sequences: true, + passes: 2, + }; + input: { + function f() { + (function(){ return true })() ? console.log(true) : console.log(false); + (function(){ + console.log("something"); + })(); + } + function g() { + (function(){ + console.log("something"); + })(); + (function(){ return true })() ? console.log(true) : console.log(false); + } + } + expect: { + function f() { + !function(){ return true }() ? console.log(false) : console.log(true), function(){ + console.log("something"); + }(); + } + function g() { + (function(){ + console.log("something"); + })(), function(){ return true }() ? console.log(true) : console.log(false); + } + } +} + negate_iife_4: { options = { negate_iife: true, @@ -75,6 +135,29 @@ negate_iife_4: { } } +negate_iife_4_off: { + options = { + negate_iife: false, + sequences: true, + conditionals: true, + }; + input: { + if ((function(){ return true })()) { + foo(true); + } else { + bar(false); + } + (function(){ + console.log("something"); + })(); + } + expect: { + !function(){ return true }() ? bar(false) : foo(true), function(){ + console.log("something"); + }(); + } +} + negate_iife_nested: { options = { negate_iife: true, @@ -107,6 +190,38 @@ negate_iife_nested: { } } +negate_iife_nested_off: { + options = { + negate_iife: false, + sequences: true, + conditionals: true, + }; + input: { + function Foo(f) { + this.f = f; + } + new Foo(function() { + (function(x) { + (function(y) { + console.log(y); + })(x); + })(7); + }).f(); + } + expect: { + function Foo(f) { + this.f = f; + } + new Foo(function() { + (function(x) { + (function(y) { + console.log(y); + })(x); + })(7); + }).f(); + } +} + negate_iife_issue_1073: { options = { negate_iife: true, @@ -172,3 +287,36 @@ issue_1254_negate_iife_nested: { } expect_exact: '!function(){return function(){console.log("test")}}()()()()();' } + +issue_1288: { + options = { + negate_iife: true, + conditionals: true, + }; + input: { + if (w) ; + else { + (function f() {})(); + } + if (!x) { + (function() { + x = {}; + })(); + } + if (y) + (function() {})(); + else + (function(z) { + return z; + })(0); + } + expect: { + w || function f() {}(); + x || function() { + x = {}; + }(); + y ? function() {}() : function(z) { + return z; + }(0); + } +} diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 8a3ffe89..d93f5237 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -213,3 +213,41 @@ limit_2: { i, j, k; } } + +negate_iife_for: { + options = { + sequences: true, + negate_iife: true, + }; + input: { + (function() {})(); + for (i = 0; i < 5; i++) console.log(i); + + (function() {})(); + for (; i < 5; i++) console.log(i); + } + expect: { + for (!function() {}(), i = 0; i < 5; i++) console.log(i); + for (function() {}(); i < 5; i++) console.log(i); + } +} + +iife: { + options = { + sequences: true, + }; + input: { + x = 42; + (function a() {})(); + !function b() {}(); + ~function c() {}(); + +function d() {}(); + -function e() {}(); + void function f() {}(); + typeof function g() {}(); + } + expect: { + x = 42, function a() {}(), function b() {}(), function c() {}(), + function d() {}(), function e() {}(), function f() {}(), function g() {}() + } +} From 6ffbecb72b515d6e9e6dee1f76d8f27e4b014854 Mon Sep 17 00:00:00 2001 From: kzc Date: Sat, 18 Feb 2017 19:12:57 +0800 Subject: [PATCH 13/28] smarter const replacement taking name length into account closes #1459 --- lib/compress.js | 12 +++ test/compress/const.js | 161 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 test/compress/const.js diff --git a/lib/compress.js b/lib/compress.js index b66c5582..1f710f10 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2784,6 +2784,18 @@ merge(Compressor.prototype, { } } } + if (compressor.option("evaluate") && !isLHS(self, compressor.parent())) { + var d = self.definition(); + if (d && d.constant && d.init && d.init.is_constant(compressor)) { + var original_as_string = self.print_to_string(); + var const_node = make_node_from_constant(compressor, d.init.constant_value(compressor), self); + var const_node_as_string = const_node.print_to_string(); + var per_const_overhead = d.global || !d.references.length ? 0 + : (d.name.length + 2 + const_node_as_string.length) / d.references.length; + if (const_node_as_string.length <= original_as_string.length + per_const_overhead) + return const_node; + } + } return self; }); diff --git a/test/compress/const.js b/test/compress/const.js new file mode 100644 index 00000000..dd175fcc --- /dev/null +++ b/test/compress/const.js @@ -0,0 +1,161 @@ +issue_1191: { + options = { + evaluate : true, + booleans : true, + comparisons : true, + dead_code : true, + conditionals : true, + side_effects : true, + unused : true, + hoist_funs : true, + if_return : true, + join_vars : true, + sequences : false, + collapse_vars : false, + } + input: { + function foo(rot) { + const rotTol = 5; + if (rot < -rotTol || rot > rotTol) + bar(); + baz(); + } + } + expect: { + function foo(rot) { + (rot < -5 || rot > 5) && bar(); + baz(); + } + } +} + +issue_1194: { + options = { + evaluate : true, + booleans : true, + comparisons : true, + dead_code : true, + conditionals : true, + side_effects : true, + unused : true, + hoist_funs : true, + if_return : true, + join_vars : true, + sequences : false, + collapse_vars : false, + } + input: { + function f1() {const a = "X"; return a + a;} + function f2() {const aa = "X"; return aa + aa;} + function f3() {const aaa = "X"; return aaa + aaa;} + } + expect: { + function f1(){return"XX"} + function f2(){return"XX"} + function f3(){return"XX"} + } +} + +issue_1396: { + options = { + evaluate : true, + booleans : true, + comparisons : true, + dead_code : true, + conditionals : true, + side_effects : true, + unused : true, + hoist_funs : true, + if_return : true, + join_vars : true, + sequences : false, + collapse_vars : false, + } + input: { + function foo(a) { + const VALUE = 1; + console.log(2 | VALUE); + console.log(VALUE + 1); + console.log(VALUE); + console.log(a & VALUE); + } + function bar() { + const s = "01234567890123456789"; + console.log(s + s + s + s + s); + + const CONSTANT = "abc"; + console.log(CONSTANT + CONSTANT + CONSTANT + CONSTANT + CONSTANT); + } + } + expect: { + function foo(a) { + console.log(3); + console.log(2); + console.log(1); + console.log(1 & a); + } + function bar() { + const s = "01234567890123456789"; + console.log(s + s + s + s + s); + + console.log("abcabcabcabcabc"); + } + } +} + +unused_regexp_literal: { + options = { + evaluate : true, + booleans : true, + comparisons : true, + dead_code : true, + conditionals : true, + side_effects : true, + unused : true, + hoist_funs : true, + if_return : true, + join_vars : true, + sequences : false, + collapse_vars : false, + } + input: { + function f(){ var a = /b/; } + } + expect: { + function f(){} + } +} + +regexp_literal_not_const: { + options = { + evaluate : true, + booleans : true, + comparisons : true, + dead_code : true, + conditionals : true, + side_effects : true, + unused : true, + hoist_funs : true, + if_return : true, + join_vars : true, + sequences : false, + collapse_vars : false, + } + input: { + (function(){ + var result; + const s = 'acdabcdeabbb'; + const REGEXP_LITERAL = /ab*/g; + while (result = REGEXP_LITERAL.exec(s)) { + console.log(result[0]); + } + })(); + } + expect: { + (function() { + var result; + const REGEXP_LITERAL = /ab*/g; + while (result = REGEXP_LITERAL.exec("acdabcdeabbb")) console.log(result[0]); + })(); + } +} From c525a2b1907fdef36acffdeea4cf02ae476d8399 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:15:09 +0800 Subject: [PATCH 14/28] fix duplicated test names previously test cases with the same name would be skipped except for the last one `test/run-test.js` will now report duplicated names as errors closes #1461 --- test/compress/drop-console.js | 2 +- test/compress/hoist_vars.js | 90 +++++++++++++++++++++++++++++++++++ test/compress/negate-iife.js | 6 +-- test/run-tests.js | 3 ++ 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 test/compress/hoist_vars.js diff --git a/test/compress/drop-console.js b/test/compress/drop-console.js index 162b339c..2333722f 100644 --- a/test/compress/drop-console.js +++ b/test/compress/drop-console.js @@ -10,7 +10,7 @@ drop_console_1: { } } -drop_console_1: { +drop_console_2: { options = { drop_console: true }; input: { console.log('foo'); diff --git a/test/compress/hoist_vars.js b/test/compress/hoist_vars.js new file mode 100644 index 00000000..6fe1c773 --- /dev/null +++ b/test/compress/hoist_vars.js @@ -0,0 +1,90 @@ +statements: { + options = { + hoist_funs: false, + hoist_vars: true, + } + input: { + function f() { + var a = 1; + var b = 2; + var c = 3; + function g() {} + return g(a, b, c); + } + } + expect: { + function f() { + var a = 1, b = 2, c = 3; + function g() {} + return g(a, b, c); + } + } +} + +statements_funs: { + options = { + hoist_funs: true, + hoist_vars: true, + } + input: { + function f() { + var a = 1; + var b = 2; + var c = 3; + function g() {} + return g(a, b, c); + } + } + expect: { + function f() { + function g() {} + var a = 1, b = 2, c = 3; + return g(a, b, c); + } + } +} + +sequences: { + options = { + hoist_funs: false, + hoist_vars: true, + } + input: { + function f() { + var a = 1, b = 2; + function g() {} + var c = 3; + return g(a, b, c); + } + } + expect: { + function f() { + var c, a = 1, b = 2; + function g() {} + c = 3; + return g(a, b, c); + } + } +} + +sequences_funs: { + options = { + hoist_funs: true, + hoist_vars: true, + } + input: { + function f() { + var a = 1, b = 2; + function g() {} + var c = 3; + return g(a, b, c); + } + } + expect: { + function f() { + function g() {} + var a = 1, b = 2, c = 3; + return g(a, b, c); + } + } +} diff --git a/test/compress/negate-iife.js b/test/compress/negate-iife.js index 312e0f28..001795c5 100644 --- a/test/compress/negate-iife.js +++ b/test/compress/negate-iife.js @@ -58,7 +58,7 @@ negate_iife_3_off: { } } -negate_iife_3: { +negate_iife_4: { options = { negate_iife: true, conditionals: true, @@ -112,7 +112,7 @@ sequence_off: { } } -negate_iife_4: { +negate_iife_5: { options = { negate_iife: true, sequences: true, @@ -135,7 +135,7 @@ negate_iife_4: { } } -negate_iife_4_off: { +negate_iife_5_off: { options = { negate_iife: false, sequences: true, diff --git a/test/run-tests.js b/test/run-tests.js index a4721399..15a12c6b 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -194,6 +194,9 @@ function parse_test(file) { if (node instanceof U.AST_LabeledStatement && tw.parent() instanceof U.AST_Toplevel) { var name = node.label.name; + if (name in tests) { + throw new Error('Duplicated test name "' + name + '" in ' + file); + } tests[name] = get_one_test(name, node.body); return true; } From b8b133d91a7a65f3375d391a036623901d1e357f Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:19:12 +0800 Subject: [PATCH 15/28] improve keep_fargs & keep_fnames - utilise in_use_ids instead of unreferenced() - drop_unused now up-to-date for subsequent passes closes #1476 --- lib/compress.js | 20 ++++++++------------ test/compress/drop-unused.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 1f710f10..345a414a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1420,7 +1420,7 @@ merge(Compressor.prototype, { drop_funcs = drop_vars = true; } var in_use = []; - var in_use_ids = {}; // avoid expensive linear scans of 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)) { @@ -1514,11 +1514,17 @@ merge(Compressor.prototype, { // 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") + && !(node.name.definition().id in in_use_ids)) { + node.name = null; + } if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { if (!compressor.option("keep_fargs")) { for (var a = node.argnames, i = a.length; --i >= 0;) { var sym = a[i]; - if (sym.unreferenced()) { + if (!(sym.definition().id in in_use_ids)) { a.pop(); compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { name : sym.name, @@ -2145,16 +2151,6 @@ merge(Compressor.prototype, { return self; }); - OPT(AST_Function, function(self, compressor){ - self = AST_Lambda.prototype.optimize.call(self, compressor); - if (compressor.option("unused") && !compressor.option("keep_fnames")) { - if (self.name && self.name.unreferenced()) { - self.name = null; - } - } - return self; - }); - OPT(AST_Call, function(self, compressor){ if (compressor.option("unsafe")) { var exp = self.expression; diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 5620cf40..5a09c6cd 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -556,3 +556,37 @@ drop_toplevel_keep_assign: { console.log(b = 3); } } + +drop_fargs: { + options = { + keep_fargs: false, + unused: true, + } + input: { + function f(a) { + var b = a; + } + } + expect: { + function f() {} + } +} + +drop_fnames: { + options = { + keep_fnames: false, + unused: true, + } + input: { + function f() { + return function g() { + var a = g; + }; + } + } + expect: { + function f() { + return function() {}; + } + } +} From a0f4fd390a0a1af80964aab9754bf5358db575e2 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:19:55 +0800 Subject: [PATCH 16/28] improve reduce_vars and fix a bug - update modified flag between compress() passes - support IIFE arguments - fix corner case with multiple definitions closes #1473 --- lib/compress.js | 41 ++++++++++-- lib/scope.js | 26 ++------ test/compress/reduce_vars.js | 124 ++++++++++++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 30 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 345a414a..72afe92a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -108,7 +108,8 @@ merge(Compressor.prototype, { compress: function(node) { var passes = +this.options.passes || 1; for (var pass = 0; pass < passes && pass < 3; ++pass) { - if (pass > 0) node.clear_opt_flags(); + if (pass > 0 || this.option("reduce_vars")) + node.reset_opt_flags(this); node = node.transform(this); } return node; @@ -167,19 +168,45 @@ merge(Compressor.prototype, { return this.print_to_string() == node.print_to_string(); }); - AST_Node.DEFMETHOD("clear_opt_flags", function(){ - this.walk(new TreeWalker(function(node){ + AST_Node.DEFMETHOD("reset_opt_flags", function(compressor){ + var reduce_vars = compressor.option("reduce_vars"); + var tw = new TreeWalker(function(node){ + if (reduce_vars && node instanceof AST_Scope) { + node.variables.each(function(def) { + delete def.modified; + }); + } if (node instanceof AST_SymbolRef) { var d = node.definition(); - if (d && d.init) { + if (d.init) { delete d.init._evaluated; } + if (reduce_vars && (d.orig.length > 1 || isModified(node, 0))) { + d.modified = true; + } + } + if (reduce_vars && node instanceof AST_Call && node.expression instanceof AST_Function) { + node.expression.argnames.forEach(function(arg, i) { + arg.definition().init = node.args[i] || make_node(AST_Undefined, node); + }); } if (!(node instanceof AST_Directive || node instanceof AST_Constant)) { node._squeezed = false; node._optimized = false; } - })); + }); + this.walk(tw); + + function isModified(node, level) { + var parent = tw.parent(level); + if (parent instanceof AST_Unary && (parent.operator === "++" || parent.operator === "--") + || parent instanceof AST_Assign && parent.left === node + || parent instanceof AST_Call && parent.expression === node) { + return true; + } else if (parent instanceof AST_PropAccess && parent.expression === node) { + return isModified(parent, level + 1); + } + } }); function make_node(ctor, orig, props) { @@ -459,7 +486,7 @@ merge(Compressor.prototype, { var_defs_removed = true; } // Further optimize statement after substitution. - stat.clear_opt_flags(); + stat.reset_opt_flags(compressor); compressor.warn("Replacing " + (is_constant ? "constant" : "variable") + " " + var_name + " [{file}:{line},{col}]", node.start); @@ -1158,7 +1185,7 @@ merge(Compressor.prototype, { this._evaluating = true; try { var d = this.definition(); - if (d && (d.constant || compressor.option("reduce_vars") && !d.modified) && d.init) { + if ((d.constant || compressor.option("reduce_vars") && !d.modified) && d.init) { if (compressor.option("unsafe")) { if (!HOP(d.init, '_evaluated')) { d.init._evaluated = ev(d.init, compressor); diff --git a/lib/scope.js b/lib/scope.js index d5cadd34..6ad12616 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -183,17 +183,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var func = null; var globals = self.globals = new Dictionary(); var tw = new TreeWalker(function(node, descend){ - function isModified(node, level) { - var parent = tw.parent(level); - if (parent instanceof AST_Unary && (parent.operator === "++" || parent.operator === "--") - || parent instanceof AST_Assign && parent.left === node - || parent instanceof AST_Call && parent.expression === node) { - return true; - } else if (parent instanceof AST_PropAccess && parent.expression === node) { - return isModified(parent, level + 1); - } - } - if (node instanceof AST_Lambda) { var prev_func = func; func = node; @@ -217,21 +206,16 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.scope.uses_arguments = true; } if (!sym) { - var g; if (globals.has(name)) { - g = globals.get(name); + sym = globals.get(name); } else { - g = new SymbolDef(self, globals.size(), node); - g.undeclared = true; - g.global = true; - globals.set(name, g); + sym = new SymbolDef(self, globals.size(), node); + sym.undeclared = true; + sym.global = true; + globals.set(name, sym); } - sym = g; } node.thedef = sym; - if (isModified(node, 0)) { - sym.modified = true; - } node.reference(options); return true; } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 2301a92a..d9d02efa 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -108,8 +108,6 @@ modified: { } console.log(a + b); console.log(b + c); - // TODO: as "modified" is determined in "figure_out_scope", - // even "passes" wouldn't improve this any further console.log(a + c); console.log(a + b + c); } @@ -350,3 +348,125 @@ unsafe_evaluate_equality: { } } } + +passes: { + options = { + conditionals: true, + evaluate: true, + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + function f() { + var a = 1, b = 2, c = 3; + if (a) { + b = c; + } else { + c = b; + } + console.log(a + b); + console.log(b + c); + console.log(a + c); + console.log(a + b + c); + } + } + expect: { + function f() { + var b = 2, c = 3; + b = c; + console.log(1 + b); + console.log(b + 3); + console.log(4); + console.log(1 + b + 3); + } + } +} + +iife: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + !function(a, b, c) { + b++; + console.log(a - 1, b * 1, c + 2); + }(1, 2, 3); + } + expect: { + !function(a, b, c) { + b++; + console.log(0, 1 * b, 5); + }(1, 2, 3); + } +} + +iife_new: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + var A = new function(a, b, c) { + b++; + console.log(a - 1, b * 1, c + 2); + }(1, 2, 3); + } + expect: { + var A = new function(a, b, c) { + b++; + console.log(0, 1 * b, 5); + }(1, 2, 3); + } +} + +multi_def: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + function f(a) { + if (a) + var b = 1; + else + var b = 2 + console.log(b + 1); + } + } + expect: { + function f(a) { + if (a) + var b = 1; + else + var b = 2 + console.log(b + 1); + } + } +} + +multi_def_2: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + if (code == 16) + var bitsLength = 2, bitsOffset = 3, what = len; + else if (code == 17) + var bitsLength = 3, bitsOffset = 3, what = (len = 0); + else if (code == 18) + var bitsLength = 7, bitsOffset = 11, what = (len = 0); + var repeatLength = this.getBits(bitsLength) + bitsOffset; + } + expect: { + if (16 == code) + var bitsLength = 2, bitsOffset = 3, what = len; + else if (17 == code) + var bitsLength = 3, bitsOffset = 3, what = (len = 0); + else if (18 == code) + var bitsLength = 7, bitsOffset = 11, what = (len = 0); + var repeatLength = this.getBits(bitsLength) + bitsOffset; + } +} From 974247c8c0e57901ef776e86784c8c9a1b87b5de Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:22:24 +0800 Subject: [PATCH 17/28] evaluate AST_SymbolRef as parameter fix invalid boolean conversion now exposed in `make_node_from_constant()` closes #1477 --- lib/compress.js | 15 ++++++++----- test/compress/evaluate.js | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 72afe92a..a60ba1a1 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -234,7 +234,7 @@ merge(Compressor.prototype, { case "string": return make_node(AST_String, orig, { value: val - }).optimize(compressor); + }); case "number": if (isNaN(val)) { return make_node(AST_NaN, orig); @@ -247,17 +247,17 @@ merge(Compressor.prototype, { }); } - return make_node(AST_Number, orig, { value: val }).optimize(compressor); + return make_node(AST_Number, orig, { value: val }); case "boolean": - return make_node(val ? AST_True : AST_False, orig).optimize(compressor); + return make_node(val ? AST_True : AST_False, orig).transform(compressor); case "undefined": - return make_node(AST_Undefined, orig).optimize(compressor); + return make_node(AST_Undefined, orig).transform(compressor); default: if (val === null) { - return make_node(AST_Null, orig, { value: null }).optimize(compressor); + return make_node(AST_Null, orig, { value: null }); } if (val instanceof RegExp) { - return make_node(AST_RegExp, orig, { value: val }).optimize(compressor); + return make_node(AST_RegExp, orig, { value: val }); } throw new Error(string_template("Can't handle constant of type: {type}", { type: typeof val @@ -2179,6 +2179,9 @@ merge(Compressor.prototype, { }); OPT(AST_Call, function(self, compressor){ + self.args = self.args.map(function(arg) { + return arg.evaluate(compressor)[0]; + }); if (compressor.option("unsafe")) { var exp = self.expression; if (exp instanceof AST_SymbolRef && exp.undeclared()) { diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 0ff157dc..f88bc538 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -598,3 +598,50 @@ unsafe_prototype_function: { var h = "" + ({toString: 0}); } } + +call_args: { + options = { + evaluate: true, + } + input: { + const a = 1; + console.log(a); + +function(a) { + return a; + }(a); + } + expect: { + const a = 1; + console.log(1); + +function(a) { + return a; + }(1); + } +} + +in_boolean_context: { + options = { + booleans: true, + evaluate: true, + } + input: { + !42; + !"foo"; + ![1, 2]; + !/foo/; + !b(42); + !b("foo"); + !b([1, 2]); + !b(/foo/); + } + expect: { + !1; + !1; + !1; + !1; + !b(42); + !b("foo"); + !b([1, 2]); + !b(/foo/); + } +} From e275148998638bdcf795257ed03941ca34e33018 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:27:31 +0800 Subject: [PATCH 18/28] enhance `global_defs` - support arrays, objects & AST_Node - support `"a.b":1` on both cli & API - emit warning if variable is modified - override top-level variables fixes #1416 closes #1198 closes #1469 --- README.md | 2 + lib/compress.js | 114 ++++++++++++++++++------ lib/scope.js | 22 +++-- test/compress/global_defs.js | 147 +++++++++++++++++++++++++++++++ test/compress/issue-208.js | 41 +++++++++ test/input/global_defs/nested.js | 1 + test/input/global_defs/simple.js | 1 + test/mocha/cli.js | 30 +++++++ 8 files changed, 324 insertions(+), 34 deletions(-) create mode 100644 test/compress/global_defs.js create mode 100644 test/input/global_defs/nested.js create mode 100644 test/input/global_defs/simple.js diff --git a/README.md b/README.md index a2eaeae4..1d1f2fcb 100644 --- a/README.md +++ b/README.md @@ -454,6 +454,8 @@ if (DEBUG) { } ``` +You can specify nested constants in the form of `--define env.DEBUG=false`. + UglifyJS will warn about the condition being always false and about dropping unreachable code; for now there is no option to turn off only this specific warning, you can pass `warnings=false` to turn off *all* warnings. diff --git a/lib/compress.js b/lib/compress.js index a60ba1a1..cb99a173 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -219,17 +219,6 @@ merge(Compressor.prototype, { }; function make_node_from_constant(compressor, val, orig) { - // XXX: WIP. - // if (val instanceof AST_Node) return val.transform(new TreeTransformer(null, function(node){ - // if (node instanceof AST_SymbolRef) { - // var scope = compressor.find_parent(AST_Scope); - // var def = scope.find_variable(node); - // node.thedef = def; - // return node; - // } - // })).transform(compressor); - - if (val instanceof AST_Node) return val.transform(compressor); switch (typeof val) { case "string": return make_node(AST_String, orig, { @@ -991,6 +980,68 @@ merge(Compressor.prototype, { || parent instanceof AST_Assign && parent.left === node; } + (function (def){ + AST_Node.DEFMETHOD("resolve_defines", function(compressor) { + if (!compressor.option("global_defs")) return; + var def = this._find_defs(compressor, ""); + if (def) { + var node, parent = this, level = 0; + do { + node = parent; + parent = compressor.parent(level++); + } while (parent instanceof AST_PropAccess && parent.expression === node); + if (isLHS(node, parent)) { + compressor.warn('global_defs ' + this.print_to_string() + ' redefined [{file}:{line},{col}]', this.start); + } else { + return def; + } + } + }); + function to_node(compressor, value, orig) { + if (value instanceof AST_Node) return make_node(value.CTOR, orig, value); + if (Array.isArray(value)) return make_node(AST_Array, orig, { + elements: value.map(function(value) { + return to_node(compressor, value, orig); + }) + }); + if (value && typeof value == "object") { + var props = []; + for (var key in value) { + props.push(make_node(AST_ObjectKeyVal, orig, { + key: key, + value: to_node(compressor, value[key], orig) + })); + } + return make_node(AST_Object, orig, { + properties: props + }); + } + return make_node_from_constant(compressor, value, orig); + } + def(AST_Node, noop); + def(AST_Dot, function(compressor, suffix){ + return this.expression._find_defs(compressor, suffix + "." + this.property); + }); + def(AST_SymbolRef, function(compressor, suffix){ + if (!this.global()) return; + var name; + var defines = compressor.option("global_defs"); + if (defines && HOP(defines, (name = this.name + suffix))) { + var node = to_node(compressor, defines[name], this); + var top = compressor.find_parent(AST_Toplevel); + node.walk(new TreeWalker(function(node) { + if (node instanceof AST_SymbolRef) { + node.scope = top; + node.thedef = top.def_global(node); + } + })); + return node; + } + }); + })(function(node, func){ + node.DEFMETHOD("_find_defs", func); + }); + function best_of(ast1, ast2) { return ast1.print_to_string().length > ast2.print_to_string().length @@ -2793,21 +2844,20 @@ merge(Compressor.prototype, { }); OPT(AST_SymbolRef, function(self, compressor){ - if (self.undeclared() && !isLHS(self, compressor.parent())) { - var defines = compressor.option("global_defs"); - if (defines && HOP(defines, self.name)) { - return make_node_from_constant(compressor, defines[self.name], self); - } - // testing against !self.scope.uses_with first is an optimization - if (!self.scope.uses_with || !compressor.find_parent(AST_With)) { - switch (self.name) { - case "undefined": - return make_node(AST_Undefined, self); - case "NaN": - return make_node(AST_NaN, self).transform(compressor); - case "Infinity": - return make_node(AST_Infinity, self).transform(compressor); - } + var def = self.resolve_defines(compressor); + if (def) { + return def; + } + // testing against !self.scope.uses_with first is an optimization + if (self.undeclared() && !isLHS(self, compressor.parent()) + && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { + switch (self.name) { + case "undefined": + return make_node(AST_Undefined, self); + case "NaN": + return make_node(AST_NaN, self).transform(compressor); + case "Infinity": + return make_node(AST_Infinity, self).transform(compressor); } } if (compressor.option("evaluate") && !isLHS(self, compressor.parent())) { @@ -3085,6 +3135,10 @@ merge(Compressor.prototype, { }); OPT(AST_Dot, function(self, compressor){ + var def = self.resolve_defines(compressor); + if (def) { + return def; + } var prop = self.property; if (RESERVED_WORDS(prop) && !compressor.option("screw_ie8")) { return make_node(AST_Sub, self, { @@ -3114,4 +3168,12 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_VarDef, function(self, compressor){ + var defines = compressor.option("global_defs"); + if (defines && HOP(defines, self.name.name)) { + compressor.warn('global_defs ' + self.name.name + ' redefined [{file}:{line},{col}]', self.start); + } + return self; + }); + })(); diff --git a/lib/scope.js b/lib/scope.js index 6ad12616..b0d92d7d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -206,14 +206,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.scope.uses_arguments = true; } if (!sym) { - if (globals.has(name)) { - sym = globals.get(name); - } else { - sym = new SymbolDef(self, globals.size(), node); - sym.undeclared = true; - sym.global = true; - globals.set(name, sym); - } + sym = self.def_global(node); } node.thedef = sym; node.reference(options); @@ -227,6 +220,19 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } }); +AST_Toplevel.DEFMETHOD("def_global", function(node){ + var globals = this.globals, name = node.name; + if (globals.has(name)) { + return globals.get(name); + } else { + var g = new SymbolDef(this, globals.size(), node); + g.undeclared = true; + g.global = true; + globals.set(name, g); + return g; + } +}); + AST_Scope.DEFMETHOD("init_scope_vars", function(){ this.variables = new Dictionary(); // map name to AST_SymbolVar (variables defined in this scope; includes functions) this.functions = new Dictionary(); // map name to AST_SymbolDefun (functions defined in this scope) diff --git a/test/compress/global_defs.js b/test/compress/global_defs.js new file mode 100644 index 00000000..a69d031e --- /dev/null +++ b/test/compress/global_defs.js @@ -0,0 +1,147 @@ +must_replace: { + options = { + global_defs: { + D: "foo bar", + } + } + input: { + console.log(D); + } + expect: { + console.log("foo bar"); + } +} + +keyword: { + options = { + global_defs: { + undefined: 0, + NaN: 1, + Infinity: 2, + }, + } + input: { + console.log(undefined, NaN, Infinity); + } + expect: { + console.log(0, 1, 2); + } +} + +object: { + options = { + evaluate: true, + global_defs: { + CONFIG: { + DEBUG: [ 0 ], + VALUE: 42, + }, + }, + unsafe: true, + } + input: { + function f(CONFIG) { + // CONFIG not global - do not replace + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + // CONFIG not global - do not replace + return CONFIG.VALUE; + } + function h() { + return CONFIG.VALUE; + } + if (CONFIG.DEBUG[0]) + console.debug("foo"); + } + expect: { + function f(CONFIG) { + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + return CONFIG.VALUE; + } + function h() { + return 42; + } + if (0) + console.debug("foo"); + } +} + +expanded: { + options = { + global_defs: { + "CONFIG.DEBUG": [ 0 ], + "CONFIG.VALUE": 42, + }, + } + input: { + function f(CONFIG) { + // CONFIG not global - do not replace + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + // CONFIG not global - do not replace + return CONFIG.VALUE; + } + function h() { + return CONFIG.VALUE; + } + if (CONFIG.DEBUG[0]) + console.debug("foo"); + } + expect: { + function f(CONFIG) { + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + return CONFIG.VALUE; + } + function h() { + return 42; + } + if ([0][0]) + console.debug("foo"); + } +} + +mixed: { + options = { + evaluate: true, + global_defs: { + "CONFIG.VALUE": 42, + "FOO.BAR": "moo", + }, + properties: true, + } + input: { + const FOO = { BAR: 0 }; + console.log(FOO.BAR); + console.log(++CONFIG.DEBUG); + console.log(++CONFIG.VALUE); + console.log(++CONFIG["VAL" + "UE"]); + console.log(++DEBUG[CONFIG.VALUE]); + CONFIG.VALUE.FOO = "bar"; + console.log(CONFIG); + } + expect: { + const FOO = { BAR: 0 }; + console.log("moo"); + console.log(++CONFIG.DEBUG); + console.log(++CONFIG.VALUE); + console.log(++CONFIG.VALUE); + console.log(++DEBUG[42]); + CONFIG.VALUE.FOO = "bar"; + console.log(CONFIG); + } + expect_warnings: [ + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:126,22]', + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:127,22]', + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:129,8]', + ] +} diff --git a/test/compress/issue-208.js b/test/compress/issue-208.js index 2f103786..fb9861f6 100644 --- a/test/compress/issue-208.js +++ b/test/compress/issue-208.js @@ -27,3 +27,44 @@ do_update_rhs: { MY_DEBUG += 0; } } + +mixed: { + options = { + evaluate: true, + global_defs: { + DEBUG: 0, + ENV: 1, + FOO: 2, + } + } + input: { + const ENV = 3; + var FOO = 4; + f(ENV * 10); + --FOO; + DEBUG = 1; + DEBUG++; + DEBUG += 1; + f(DEBUG); + x = DEBUG; + } + expect: { + const ENV = 3; + var FOO = 4; + f(10); + --FOO; + DEBUG = 1; + DEBUG++; + DEBUG += 1; + f(0); + x = 0; + } + expect_warnings: [ + 'WARN: global_defs ENV redefined [test/compress/issue-208.js:41,14]', + 'WARN: global_defs FOO redefined [test/compress/issue-208.js:42,12]', + 'WARN: global_defs FOO redefined [test/compress/issue-208.js:44,10]', + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:45,8]', + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:46,8]', + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:47,8]', + ] +} diff --git a/test/input/global_defs/nested.js b/test/input/global_defs/nested.js new file mode 100644 index 00000000..dbf57909 --- /dev/null +++ b/test/input/global_defs/nested.js @@ -0,0 +1 @@ +console.log(C.V, C.D); diff --git a/test/input/global_defs/simple.js b/test/input/global_defs/simple.js new file mode 100644 index 00000000..44d515e3 --- /dev/null +++ b/test/input/global_defs/simple.js @@ -0,0 +1 @@ +console.log(D); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index c5b571bd..64599c51 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -100,4 +100,34 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should work with --define (simple)", function (done) { + var command = uglifyjscmd + ' test/input/global_defs/simple.js --define D=5 -c'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "console.log(5);\n"); + done(); + }); + }); + it("Should work with --define (nested)", function (done) { + var command = uglifyjscmd + ' test/input/global_defs/nested.js --define C.D=5,C.V=3 -c'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "console.log(3,5);\n"); + done(); + }); + }); + it("Should work with --define (AST_Node)", function (done) { + var command = uglifyjscmd + ' test/input/global_defs/simple.js --define console.log=stdout.println -c'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "stdout.println(D);\n"); + done(); + }); + }); }); From 7e6331bb397e1dec0a4e15233a2afca3a4e9daff Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:28:25 +0800 Subject: [PATCH 19/28] add benchmark & JetStream tests - `test/benchmark.js` measures performance - `test/jetstream.js` verifies correctness - configurable mangle/compress/output options closes #1479 --- test/benchmark.js | 49 ++++++++++++++++++++++++++ test/jetstream.js | 87 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 test/benchmark.js create mode 100644 test/jetstream.js diff --git a/test/benchmark.js b/test/benchmark.js new file mode 100644 index 00000000..dc176a88 --- /dev/null +++ b/test/benchmark.js @@ -0,0 +1,49 @@ +#! /usr/bin/env node +// -*- js -*- + +"use strict"; + +var createHash = require("crypto").createHash; +var fork = require("child_process").fork; +var args = process.argv.slice(2); +if (!args.length) { + args.push("-mc", "warnings=false"); +} +args.push("--stats"); +var urls = [ + "https://code.jquery.com/jquery-3.1.1.js", + "https://code.angularjs.org/1.6.1/angular.js", + "https://cdnjs.cloudflare.com/ajax/libs/mathjs/3.9.0/math.js", + "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js", + "https://unpkg.com/react@15.3.2/dist/react.js", + "http://builds.emberjs.com/tags/v2.11.0/ember.prod.js", + "https://cdn.jsdelivr.net/lodash/4.17.4/lodash.js", + "https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.js", +]; +var results = {}; +var remaining = 2 * urls.length; +function done() { + if (!--remaining) { + urls.forEach(function(url) { + console.log(); + console.log(url); + console.log(results[url].time); + console.log("SHA1:", results[url].sha1); + }); + } +} +urls.forEach(function(url) { + results[url] = { time: "" }; + require(url.slice(0, url.indexOf(":"))).get(url, function(res) { + var uglifyjs = fork("bin/uglifyjs", args, { silent: true }); + res.pipe(uglifyjs.stdin); + uglifyjs.stdout.pipe(createHash("sha1")).on("data", function(data) { + results[url].sha1 = data.toString("hex"); + done(); + }); + uglifyjs.stderr.setEncoding("utf8"); + uglifyjs.stderr.on("data", function(data) { + results[url].time += data; + }).on("end", done) + }); +}); diff --git a/test/jetstream.js b/test/jetstream.js new file mode 100644 index 00000000..a8195389 --- /dev/null +++ b/test/jetstream.js @@ -0,0 +1,87 @@ +#! /usr/bin/env node +// -*- js -*- + +"use strict"; + +var site = "http://browserbench.org/JetStream/"; +if (typeof phantom == "undefined") { + // workaround for tty output truncation upon process.exit() + [process.stdout, process.stderr].forEach(function(stream){ + if (stream._handle && stream._handle.setBlocking) + stream._handle.setBlocking(true); + }); + var args = process.argv.slice(2); + if (!args.length) { + args.push("-mc", "warnings=false"); + } + args.push("--stats"); + var child_process = require("child_process"); + try { + require("phantomjs-prebuilt"); + } catch(e) { + child_process.execSync("npm install phantomjs-prebuilt@2.1.14"); + } + var http = require("http"); + var server = http.createServer(function(request, response) { + request.resume(); + var url = decodeURIComponent(request.url.slice(1)); + var stderr = ""; + var uglifyjs = child_process.fork("bin/uglifyjs", args, { + silent: true + }).on("exit", function(code) { + console.log("uglifyjs", url.indexOf(site) == 0 ? url.slice(site.length) : url, args.join(" ")); + console.log(stderr); + if (code) throw new Error("uglifyjs failed with code " + code); + }); + uglifyjs.stderr.on("data", function(data) { + stderr += data; + }).setEncoding("utf8"); + uglifyjs.stdout.pipe(response); + http.get(url, function(res) { + res.pipe(uglifyjs.stdin); + }); + }).listen().on("listening", function() { + var phantomjs = require("phantomjs-prebuilt"); + var program = phantomjs.exec(process.argv[1], server.address().port); + program.stdout.pipe(process.stdout); + program.stderr.pipe(process.stderr); + program.on("exit", function(code) { + server.close(); + if (code) throw new Error("JetStream failed!"); + console.log("JetStream completed successfully."); + }); + }); + server.timeout = 0; +} else { + var page = require("webpage").create(); + page.onError = function(msg, trace) { + var body = [ msg ]; + if (trace) trace.forEach(function(t) { + body.push(" " + (t.function || "Anonymous function") + " (" + t.file + ":" + t.line + ")"); + }); + console.error(body.join("\n")); + phantom.exit(1); + }; + var url = "http://localhost:" + require("system").args[1] + "/"; + page.onResourceRequested = function(requestData, networkRequest) { + if (/\.js$/.test(requestData.url)) + networkRequest.changeUrl(url + encodeURIComponent(requestData.url)); + } + page.onConsoleMessage = function(msg) { + if (/Error:/i.test(msg)) { + console.error(msg); + phantom.exit(1); + } + console.log(msg); + if (~msg.indexOf("Raw results:")) { + phantom.exit(); + } + }; + page.open(site, function(status) { + if (status != "success") phantomjs.exit(1); + page.evaluate(function() { + JetStream.switchToQuick(); + JetStream.start(); + }); + }); +} From 09f9ae2de9fb2a328e088ab82ad1e49731c971fb Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:30:33 +0800 Subject: [PATCH 20/28] improve `--beautify bracketize` reduce whitespaces from if-else statements fixes #1482 closes #1483 --- lib/output.js | 5 +- test/input/issue-1482/bracketize.js | 73 +++++++++++++++++++++++++++++ test/input/issue-1482/default.js | 17 +++++++ test/input/issue-1482/input.js | 12 +++++ test/mocha/cli.js | 21 +++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 test/input/issue-1482/bracketize.js create mode 100644 test/input/issue-1482/default.js create mode 100644 test/input/issue-1482/input.js diff --git a/lib/output.js b/lib/output.js index b6f0a873..4f576c96 100644 --- a/lib/output.js +++ b/lib/output.js @@ -938,7 +938,10 @@ function OutputStream(options) { output.space(); output.print("else"); output.space(); - force_statement(self.alternative, output); + if (self.alternative instanceof AST_If) + self.alternative.print(output); + else + force_statement(self.alternative, output); } else { self._do_print_body(output); } diff --git a/test/input/issue-1482/bracketize.js b/test/input/issue-1482/bracketize.js new file mode 100644 index 00000000..2c2b103c --- /dev/null +++ b/test/input/issue-1482/bracketize.js @@ -0,0 +1,73 @@ +if (x) { + foo(); +} + +if (x) { + foo(); +} else { + baz(); +} + +if (x) { + foo(); +} else if (y) { + bar(); +} else { + baz(); +} + +if (x) { + if (y) { + foo(); + } else { + bar(); + } +} else { + baz(); +} + +if (x) { + foo(); +} else if (y) { + bar(); +} else if (z) { + baz(); +} else { + moo(); +} + +function f() { + if (x) { + foo(); + } + if (x) { + foo(); + } else { + baz(); + } + if (x) { + foo(); + } else if (y) { + bar(); + } else { + baz(); + } + if (x) { + if (y) { + foo(); + } else { + bar(); + } + } else { + baz(); + } + if (x) { + foo(); + } else if (y) { + bar(); + } else if (z) { + baz(); + } else { + moo(); + } +} diff --git a/test/input/issue-1482/default.js b/test/input/issue-1482/default.js new file mode 100644 index 00000000..14054e98 --- /dev/null +++ b/test/input/issue-1482/default.js @@ -0,0 +1,17 @@ +if (x) foo(); + +if (x) foo(); else baz(); + +if (x) foo(); else if (y) bar(); else baz(); + +if (x) if (y) foo(); else bar(); else baz(); + +if (x) foo(); else if (y) bar(); else if (z) baz(); else moo(); + +function f() { + if (x) foo(); + if (x) foo(); else baz(); + if (x) foo(); else if (y) bar(); else baz(); + if (x) if (y) foo(); else bar(); else baz(); + if (x) foo(); else if (y) bar(); else if (z) baz(); else moo(); +} diff --git a/test/input/issue-1482/input.js b/test/input/issue-1482/input.js new file mode 100644 index 00000000..0186e82c --- /dev/null +++ b/test/input/issue-1482/input.js @@ -0,0 +1,12 @@ +if (x) foo(); +if (x) foo(); else baz(); +if (x) foo(); else if (y) bar(); else baz(); +if (x) if (y) foo(); else bar(); else baz(); +if (x) foo(); else if (y) bar(); else if (z) baz(); else moo(); +function f() { +if (x) foo(); +if (x) foo(); else baz(); +if (x) foo(); else if (y) bar(); else baz(); +if (x) if (y) foo(); else bar(); else baz(); +if (x) foo(); else if (y) bar(); else if (z) baz(); else moo(); +} diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 64599c51..450df1fd 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -1,5 +1,6 @@ var assert = require("assert"); var exec = require("child_process").exec; +var readFileSync = require("fs").readFileSync; describe("bin/uglifyjs", function () { var uglifyjscmd = '"' + process.argv[0] + '" bin/uglifyjs'; @@ -130,4 +131,24 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should work with `--beautify`", function (done) { + var command = uglifyjscmd + ' test/input/issue-1482/input.js -b'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, readFileSync("test/input/issue-1482/default.js", "utf8")); + done(); + }); + }); + it("Should work with `--beautify bracketize`", function (done) { + var command = uglifyjscmd + ' test/input/issue-1482/input.js -b bracketize'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, readFileSync("test/input/issue-1482/bracketize.js", "utf8")); + done(); + }); + }); }); From c06a50f338c1b473f38577d720ccf9b4ff22dc4f Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 18 Feb 2017 19:33:05 +0800 Subject: [PATCH 21/28] Add .gitattributes to checkout lf eol style closes #1487 --- .gitattributes | 1 + test/compress/dead-code.js | 412 +++++++++++++++++----------------- test/compress/drop-console.js | 48 ++-- 3 files changed, 231 insertions(+), 230 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6edcf864 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.js text eol=lf diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index fa4b37d6..c83f2040 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -1,206 +1,206 @@ -dead_code_1: { - options = { - dead_code: true - }; - input: { - function f() { - a(); - b(); - x = 10; - return; - if (x) { - y(); - } - } - } - expect: { - function f() { - a(); - b(); - x = 10; - return; - } - } -} - -dead_code_2_should_warn: { - options = { - dead_code: true - }; - input: { - function f() { - g(); - x = 10; - throw "foo"; - // completely discarding the `if` would introduce some - // bugs. UglifyJS v1 doesn't deal with this issue; in v2 - // we copy any declarations to the upper scope. - if (x) { - y(); - var x; - function g(){}; - // but nested declarations should not be kept. - (function(){ - var q; - function y(){}; - })(); - } - } - } - expect: { - function f() { - g(); - x = 10; - throw "foo"; - var x; - function g(){}; - } - } -} - -dead_code_constant_boolean_should_warn_more: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - while (!((foo && bar) || (x + "0"))) { - console.log("unreachable"); - var foo; - function bar() {} - } - for (var x = 10, y; x && (y || x) && (!typeof x); ++x) { - asdf(); - foo(); - var moo; - } - } - expect: { - var foo; - function bar() {} - // nothing for the while - // as for the for, it should keep: - var x = 10, y; - var moo; - } -} - -dead_code_const_declaration: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - var unused; - const CONST_FOO = false; - if (CONST_FOO) { - console.log("unreachable"); - var moo; - function bar() {} - } - } - expect: { - var unused; - const CONST_FOO = !1; - var moo; - function bar() {} - } -} - -dead_code_const_annotation: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - var unused; - /** @const */ var CONST_FOO_ANN = false; - if (CONST_FOO_ANN) { - console.log("unreachable"); - var moo; - function bar() {} - } - } - expect: { - var unused; - var CONST_FOO_ANN = !1; - var moo; - function bar() {} - } -} - -dead_code_const_annotation_regex: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - var unused; - // @constraint this shouldn't be a constant - var CONST_FOO_ANN = false; - if (CONST_FOO_ANN) { - console.log("reachable"); - } - } - expect: { - var unused; - var CONST_FOO_ANN = !1; - CONST_FOO_ANN && console.log('reachable'); - } -} - -dead_code_const_annotation_complex_scope: { - options = { - dead_code : true, - loops : true, - booleans : true, - conditionals : true, - evaluate : true - }; - input: { - var unused_var; - /** @const */ var test = 'test'; - // @const - var CONST_FOO_ANN = false; - var unused_var_2; - if (CONST_FOO_ANN) { - console.log("unreachable"); - var moo; - function bar() {} - } - if (test === 'test') { - var beef = 'good'; - /** @const */ var meat = 'beef'; - var pork = 'bad'; - if (meat === 'pork') { - console.log('also unreachable'); - } else if (pork === 'good') { - console.log('reached, not const'); - } - } - } - expect: { - var unused_var; - var test = 'test'; - var CONST_FOO_ANN = !1; - var unused_var_2; - var moo; - function bar() {} - var beef = 'good'; - var meat = 'beef'; - var pork = 'bad'; - 'good' === pork && console.log('reached, not const'); - } -} +dead_code_1: { + options = { + dead_code: true + }; + input: { + function f() { + a(); + b(); + x = 10; + return; + if (x) { + y(); + } + } + } + expect: { + function f() { + a(); + b(); + x = 10; + return; + } + } +} + +dead_code_2_should_warn: { + options = { + dead_code: true + }; + input: { + function f() { + g(); + x = 10; + throw "foo"; + // completely discarding the `if` would introduce some + // bugs. UglifyJS v1 doesn't deal with this issue; in v2 + // we copy any declarations to the upper scope. + if (x) { + y(); + var x; + function g(){}; + // but nested declarations should not be kept. + (function(){ + var q; + function y(){}; + })(); + } + } + } + expect: { + function f() { + g(); + x = 10; + throw "foo"; + var x; + function g(){}; + } + } +} + +dead_code_constant_boolean_should_warn_more: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + while (!((foo && bar) || (x + "0"))) { + console.log("unreachable"); + var foo; + function bar() {} + } + for (var x = 10, y; x && (y || x) && (!typeof x); ++x) { + asdf(); + foo(); + var moo; + } + } + expect: { + var foo; + function bar() {} + // nothing for the while + // as for the for, it should keep: + var x = 10, y; + var moo; + } +} + +dead_code_const_declaration: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + const CONST_FOO = false; + if (CONST_FOO) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var unused; + const CONST_FOO = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + /** @const */ var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation_regex: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + // @constraint this shouldn't be a constant + var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("reachable"); + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + CONST_FOO_ANN && console.log('reachable'); + } +} + +dead_code_const_annotation_complex_scope: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused_var; + /** @const */ var test = 'test'; + // @const + var CONST_FOO_ANN = false; + var unused_var_2; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + if (test === 'test') { + var beef = 'good'; + /** @const */ var meat = 'beef'; + var pork = 'bad'; + if (meat === 'pork') { + console.log('also unreachable'); + } else if (pork === 'good') { + console.log('reached, not const'); + } + } + } + expect: { + var unused_var; + var test = 'test'; + var CONST_FOO_ANN = !1; + var unused_var_2; + var moo; + function bar() {} + var beef = 'good'; + var meat = 'beef'; + var pork = 'bad'; + 'good' === pork && console.log('reached, not const'); + } +} diff --git a/test/compress/drop-console.js b/test/compress/drop-console.js index 2333722f..7df6d9dc 100644 --- a/test/compress/drop-console.js +++ b/test/compress/drop-console.js @@ -1,24 +1,24 @@ -drop_console_1: { - options = {}; - input: { - console.log('foo'); - console.log.apply(console, arguments); - } - expect: { - console.log('foo'); - console.log.apply(console, arguments); - } -} - -drop_console_2: { - options = { drop_console: true }; - input: { - console.log('foo'); - console.log.apply(console, arguments); - } - expect: { - // with regular compression these will be stripped out as well - void 0; - void 0; - } -} +drop_console_1: { + options = {}; + input: { + console.log('foo'); + console.log.apply(console, arguments); + } + expect: { + console.log('foo'); + console.log.apply(console, arguments); + } +} + +drop_console_2: { + options = { drop_console: true }; + input: { + console.log('foo'); + console.log.apply(console, arguments); + } + expect: { + // with regular compression these will be stripped out as well + void 0; + void 0; + } +} From ac0b61ed6e1bce4794100c5bb83f6e5b22996d8a Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:33:53 +0800 Subject: [PATCH 22/28] remove extraneous spaces between ++/+/--/- fixes #1377 closes #1488 --- lib/output.js | 5 +- test/mocha/operator.js | 489 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 492 insertions(+), 2 deletions(-) create mode 100644 test/mocha/operator.js diff --git a/lib/output.js b/lib/output.js index 4f576c96..2802c305 100644 --- a/lib/output.js +++ b/lib/output.js @@ -254,8 +254,9 @@ function OutputStream(options) { if (might_need_space) { var prev = last_char(); if ((is_identifier_char(prev) - && (is_identifier_char(ch) || ch == "\\")) - || (/^[\+\-\/]$/.test(ch) && ch == prev)) + && (is_identifier_char(ch) || ch == "\\")) + || (ch == "/" && ch == prev) + || ((ch == "+" || ch == "-") && ch == last)) { OUTPUT += " "; current_col++; diff --git a/test/mocha/operator.js b/test/mocha/operator.js new file mode 100644 index 00000000..adef3abd --- /dev/null +++ b/test/mocha/operator.js @@ -0,0 +1,489 @@ +var UglifyJS = require("../../"); +var assert = require("assert"); + +describe("operator", function() { + it("Should handle mixing of ++/+/--/- correctly", function() { + function evaluate(exp) { + return new Function("var a=1,b=2,c=" + exp + ";return{a:a,b:b,c:c}")(); + } + + [ "", "+", "-" ].forEach(function(p) { + [ "++a", "--a", "a", "a--", "a++" ].forEach(function(a) { + [ "+", "-" ].forEach(function(o) { + [ "", "+", "-" ].forEach(function(q) { + [ "++b", "--b", "b", "b--", "b++" ].forEach(function(b) { + var exp = [p, a, o, q, b].join(" "); + var orig = evaluate(exp); + var uglify = evaluate(UglifyJS.parse(exp).print_to_string()); + assert.strictEqual(orig.a, uglify.a); + assert.strictEqual(orig.b, uglify.b); + assert.strictEqual(orig.c, uglify.c); + var beautify = evaluate(UglifyJS.parse(exp).print_to_string({ + beautify: true + })); + assert.strictEqual(orig.a, beautify.a); + assert.strictEqual(orig.b, beautify.b); + assert.strictEqual(orig.c, beautify.c); + }); + }); + }); + }); + }); + }); + it("Should remove extraneous spaces", function() { + [ + [ "++a + ++b", "++a+ ++b" ], + [ "++a + --b", "++a+--b" ], + [ "++a + b", "++a+b" ], + [ "++a + b--", "++a+b--" ], + [ "++a + b++", "++a+b++" ], + [ "++a + + ++b", "++a+ + ++b" ], + [ "++a + + --b", "++a+ +--b" ], + [ "++a + + b", "++a+ +b" ], + [ "++a + + b--", "++a+ +b--" ], + [ "++a + + b++", "++a+ +b++" ], + [ "++a + - ++b", "++a+-++b" ], + [ "++a + - --b", "++a+- --b" ], + [ "++a + - b", "++a+-b" ], + [ "++a + - b--", "++a+-b--" ], + [ "++a + - b++", "++a+-b++" ], + [ "++a - ++b", "++a-++b" ], + [ "++a - --b", "++a- --b" ], + [ "++a - b", "++a-b" ], + [ "++a - b--", "++a-b--" ], + [ "++a - b++", "++a-b++" ], + [ "++a - + ++b", "++a-+ ++b" ], + [ "++a - + --b", "++a-+--b" ], + [ "++a - + b", "++a-+b" ], + [ "++a - + b--", "++a-+b--" ], + [ "++a - + b++", "++a-+b++" ], + [ "++a - - ++b", "++a- -++b" ], + [ "++a - - --b", "++a- - --b" ], + [ "++a - - b", "++a- -b" ], + [ "++a - - b--", "++a- -b--" ], + [ "++a - - b++", "++a- -b++" ], + [ "--a + ++b", "--a+ ++b" ], + [ "--a + --b", "--a+--b" ], + [ "--a + b", "--a+b" ], + [ "--a + b--", "--a+b--" ], + [ "--a + b++", "--a+b++" ], + [ "--a + + ++b", "--a+ + ++b" ], + [ "--a + + --b", "--a+ +--b" ], + [ "--a + + b", "--a+ +b" ], + [ "--a + + b--", "--a+ +b--" ], + [ "--a + + b++", "--a+ +b++" ], + [ "--a + - ++b", "--a+-++b" ], + [ "--a + - --b", "--a+- --b" ], + [ "--a + - b", "--a+-b" ], + [ "--a + - b--", "--a+-b--" ], + [ "--a + - b++", "--a+-b++" ], + [ "--a - ++b", "--a-++b" ], + [ "--a - --b", "--a- --b" ], + [ "--a - b", "--a-b" ], + [ "--a - b--", "--a-b--" ], + [ "--a - b++", "--a-b++" ], + [ "--a - + ++b", "--a-+ ++b" ], + [ "--a - + --b", "--a-+--b" ], + [ "--a - + b", "--a-+b" ], + [ "--a - + b--", "--a-+b--" ], + [ "--a - + b++", "--a-+b++" ], + [ "--a - - ++b", "--a- -++b" ], + [ "--a - - --b", "--a- - --b" ], + [ "--a - - b", "--a- -b" ], + [ "--a - - b--", "--a- -b--" ], + [ "--a - - b++", "--a- -b++" ], + [ "a + ++b", "a+ ++b" ], + [ "a + --b", "a+--b" ], + [ "a + b", "a+b" ], + [ "a + b--", "a+b--" ], + [ "a + b++", "a+b++" ], + [ "a + + ++b", "a+ + ++b" ], + [ "a + + --b", "a+ +--b" ], + [ "a + + b", "a+ +b" ], + [ "a + + b--", "a+ +b--" ], + [ "a + + b++", "a+ +b++" ], + [ "a + - ++b", "a+-++b" ], + [ "a + - --b", "a+- --b" ], + [ "a + - b", "a+-b" ], + [ "a + - b--", "a+-b--" ], + [ "a + - b++", "a+-b++" ], + [ "a - ++b", "a-++b" ], + [ "a - --b", "a- --b" ], + [ "a - b", "a-b" ], + [ "a - b--", "a-b--" ], + [ "a - b++", "a-b++" ], + [ "a - + ++b", "a-+ ++b" ], + [ "a - + --b", "a-+--b" ], + [ "a - + b", "a-+b" ], + [ "a - + b--", "a-+b--" ], + [ "a - + b++", "a-+b++" ], + [ "a - - ++b", "a- -++b" ], + [ "a - - --b", "a- - --b" ], + [ "a - - b", "a- -b" ], + [ "a - - b--", "a- -b--" ], + [ "a - - b++", "a- -b++" ], + [ "a-- + ++b", "a--+ ++b" ], + [ "a-- + --b", "a--+--b" ], + [ "a-- + b", "a--+b" ], + [ "a-- + b--", "a--+b--" ], + [ "a-- + b++", "a--+b++" ], + [ "a-- + + ++b", "a--+ + ++b" ], + [ "a-- + + --b", "a--+ +--b" ], + [ "a-- + + b", "a--+ +b" ], + [ "a-- + + b--", "a--+ +b--" ], + [ "a-- + + b++", "a--+ +b++" ], + [ "a-- + - ++b", "a--+-++b" ], + [ "a-- + - --b", "a--+- --b" ], + [ "a-- + - b", "a--+-b" ], + [ "a-- + - b--", "a--+-b--" ], + [ "a-- + - b++", "a--+-b++" ], + [ "a-- - ++b", "a---++b" ], + [ "a-- - --b", "a--- --b" ], + [ "a-- - b", "a---b" ], + [ "a-- - b--", "a---b--" ], + [ "a-- - b++", "a---b++" ], + [ "a-- - + ++b", "a---+ ++b" ], + [ "a-- - + --b", "a---+--b" ], + [ "a-- - + b", "a---+b" ], + [ "a-- - + b--", "a---+b--" ], + [ "a-- - + b++", "a---+b++" ], + [ "a-- - - ++b", "a--- -++b" ], + [ "a-- - - --b", "a--- - --b" ], + [ "a-- - - b", "a--- -b" ], + [ "a-- - - b--", "a--- -b--" ], + [ "a-- - - b++", "a--- -b++" ], + [ "a++ + ++b", "a+++ ++b" ], + [ "a++ + --b", "a+++--b" ], + [ "a++ + b", "a+++b" ], + [ "a++ + b--", "a+++b--" ], + [ "a++ + b++", "a+++b++" ], + [ "a++ + + ++b", "a+++ + ++b" ], + [ "a++ + + --b", "a+++ +--b" ], + [ "a++ + + b", "a+++ +b" ], + [ "a++ + + b--", "a+++ +b--" ], + [ "a++ + + b++", "a+++ +b++" ], + [ "a++ + - ++b", "a+++-++b" ], + [ "a++ + - --b", "a+++- --b" ], + [ "a++ + - b", "a+++-b" ], + [ "a++ + - b--", "a+++-b--" ], + [ "a++ + - b++", "a+++-b++" ], + [ "a++ - ++b", "a++-++b" ], + [ "a++ - --b", "a++- --b" ], + [ "a++ - b", "a++-b" ], + [ "a++ - b--", "a++-b--" ], + [ "a++ - b++", "a++-b++" ], + [ "a++ - + ++b", "a++-+ ++b" ], + [ "a++ - + --b", "a++-+--b" ], + [ "a++ - + b", "a++-+b" ], + [ "a++ - + b--", "a++-+b--" ], + [ "a++ - + b++", "a++-+b++" ], + [ "a++ - - ++b", "a++- -++b" ], + [ "a++ - - --b", "a++- - --b" ], + [ "a++ - - b", "a++- -b" ], + [ "a++ - - b--", "a++- -b--" ], + [ "a++ - - b++", "a++- -b++" ], + [ "+ ++a + ++b", "+ ++a+ ++b" ], + [ "+ ++a + --b", "+ ++a+--b" ], + [ "+ ++a + b", "+ ++a+b" ], + [ "+ ++a + b--", "+ ++a+b--" ], + [ "+ ++a + b++", "+ ++a+b++" ], + [ "+ ++a + + ++b", "+ ++a+ + ++b" ], + [ "+ ++a + + --b", "+ ++a+ +--b" ], + [ "+ ++a + + b", "+ ++a+ +b" ], + [ "+ ++a + + b--", "+ ++a+ +b--" ], + [ "+ ++a + + b++", "+ ++a+ +b++" ], + [ "+ ++a + - ++b", "+ ++a+-++b" ], + [ "+ ++a + - --b", "+ ++a+- --b" ], + [ "+ ++a + - b", "+ ++a+-b" ], + [ "+ ++a + - b--", "+ ++a+-b--" ], + [ "+ ++a + - b++", "+ ++a+-b++" ], + [ "+ ++a - ++b", "+ ++a-++b" ], + [ "+ ++a - --b", "+ ++a- --b" ], + [ "+ ++a - b", "+ ++a-b" ], + [ "+ ++a - b--", "+ ++a-b--" ], + [ "+ ++a - b++", "+ ++a-b++" ], + [ "+ ++a - + ++b", "+ ++a-+ ++b" ], + [ "+ ++a - + --b", "+ ++a-+--b" ], + [ "+ ++a - + b", "+ ++a-+b" ], + [ "+ ++a - + b--", "+ ++a-+b--" ], + [ "+ ++a - + b++", "+ ++a-+b++" ], + [ "+ ++a - - ++b", "+ ++a- -++b" ], + [ "+ ++a - - --b", "+ ++a- - --b" ], + [ "+ ++a - - b", "+ ++a- -b" ], + [ "+ ++a - - b--", "+ ++a- -b--" ], + [ "+ ++a - - b++", "+ ++a- -b++" ], + [ "+ --a + ++b", "+--a+ ++b" ], + [ "+ --a + --b", "+--a+--b" ], + [ "+ --a + b", "+--a+b" ], + [ "+ --a + b--", "+--a+b--" ], + [ "+ --a + b++", "+--a+b++" ], + [ "+ --a + + ++b", "+--a+ + ++b" ], + [ "+ --a + + --b", "+--a+ +--b" ], + [ "+ --a + + b", "+--a+ +b" ], + [ "+ --a + + b--", "+--a+ +b--" ], + [ "+ --a + + b++", "+--a+ +b++" ], + [ "+ --a + - ++b", "+--a+-++b" ], + [ "+ --a + - --b", "+--a+- --b" ], + [ "+ --a + - b", "+--a+-b" ], + [ "+ --a + - b--", "+--a+-b--" ], + [ "+ --a + - b++", "+--a+-b++" ], + [ "+ --a - ++b", "+--a-++b" ], + [ "+ --a - --b", "+--a- --b" ], + [ "+ --a - b", "+--a-b" ], + [ "+ --a - b--", "+--a-b--" ], + [ "+ --a - b++", "+--a-b++" ], + [ "+ --a - + ++b", "+--a-+ ++b" ], + [ "+ --a - + --b", "+--a-+--b" ], + [ "+ --a - + b", "+--a-+b" ], + [ "+ --a - + b--", "+--a-+b--" ], + [ "+ --a - + b++", "+--a-+b++" ], + [ "+ --a - - ++b", "+--a- -++b" ], + [ "+ --a - - --b", "+--a- - --b" ], + [ "+ --a - - b", "+--a- -b" ], + [ "+ --a - - b--", "+--a- -b--" ], + [ "+ --a - - b++", "+--a- -b++" ], + [ "+ a + ++b", "+a+ ++b" ], + [ "+ a + --b", "+a+--b" ], + [ "+ a + b", "+a+b" ], + [ "+ a + b--", "+a+b--" ], + [ "+ a + b++", "+a+b++" ], + [ "+ a + + ++b", "+a+ + ++b" ], + [ "+ a + + --b", "+a+ +--b" ], + [ "+ a + + b", "+a+ +b" ], + [ "+ a + + b--", "+a+ +b--" ], + [ "+ a + + b++", "+a+ +b++" ], + [ "+ a + - ++b", "+a+-++b" ], + [ "+ a + - --b", "+a+- --b" ], + [ "+ a + - b", "+a+-b" ], + [ "+ a + - b--", "+a+-b--" ], + [ "+ a + - b++", "+a+-b++" ], + [ "+ a - ++b", "+a-++b" ], + [ "+ a - --b", "+a- --b" ], + [ "+ a - b", "+a-b" ], + [ "+ a - b--", "+a-b--" ], + [ "+ a - b++", "+a-b++" ], + [ "+ a - + ++b", "+a-+ ++b" ], + [ "+ a - + --b", "+a-+--b" ], + [ "+ a - + b", "+a-+b" ], + [ "+ a - + b--", "+a-+b--" ], + [ "+ a - + b++", "+a-+b++" ], + [ "+ a - - ++b", "+a- -++b" ], + [ "+ a - - --b", "+a- - --b" ], + [ "+ a - - b", "+a- -b" ], + [ "+ a - - b--", "+a- -b--" ], + [ "+ a - - b++", "+a- -b++" ], + [ "+ a-- + ++b", "+a--+ ++b" ], + [ "+ a-- + --b", "+a--+--b" ], + [ "+ a-- + b", "+a--+b" ], + [ "+ a-- + b--", "+a--+b--" ], + [ "+ a-- + b++", "+a--+b++" ], + [ "+ a-- + + ++b", "+a--+ + ++b" ], + [ "+ a-- + + --b", "+a--+ +--b" ], + [ "+ a-- + + b", "+a--+ +b" ], + [ "+ a-- + + b--", "+a--+ +b--" ], + [ "+ a-- + + b++", "+a--+ +b++" ], + [ "+ a-- + - ++b", "+a--+-++b" ], + [ "+ a-- + - --b", "+a--+- --b" ], + [ "+ a-- + - b", "+a--+-b" ], + [ "+ a-- + - b--", "+a--+-b--" ], + [ "+ a-- + - b++", "+a--+-b++" ], + [ "+ a-- - ++b", "+a---++b" ], + [ "+ a-- - --b", "+a--- --b" ], + [ "+ a-- - b", "+a---b" ], + [ "+ a-- - b--", "+a---b--" ], + [ "+ a-- - b++", "+a---b++" ], + [ "+ a-- - + ++b", "+a---+ ++b" ], + [ "+ a-- - + --b", "+a---+--b" ], + [ "+ a-- - + b", "+a---+b" ], + [ "+ a-- - + b--", "+a---+b--" ], + [ "+ a-- - + b++", "+a---+b++" ], + [ "+ a-- - - ++b", "+a--- -++b" ], + [ "+ a-- - - --b", "+a--- - --b" ], + [ "+ a-- - - b", "+a--- -b" ], + [ "+ a-- - - b--", "+a--- -b--" ], + [ "+ a-- - - b++", "+a--- -b++" ], + [ "+ a++ + ++b", "+a+++ ++b" ], + [ "+ a++ + --b", "+a+++--b" ], + [ "+ a++ + b", "+a+++b" ], + [ "+ a++ + b--", "+a+++b--" ], + [ "+ a++ + b++", "+a+++b++" ], + [ "+ a++ + + ++b", "+a+++ + ++b" ], + [ "+ a++ + + --b", "+a+++ +--b" ], + [ "+ a++ + + b", "+a+++ +b" ], + [ "+ a++ + + b--", "+a+++ +b--" ], + [ "+ a++ + + b++", "+a+++ +b++" ], + [ "+ a++ + - ++b", "+a+++-++b" ], + [ "+ a++ + - --b", "+a+++- --b" ], + [ "+ a++ + - b", "+a+++-b" ], + [ "+ a++ + - b--", "+a+++-b--" ], + [ "+ a++ + - b++", "+a+++-b++" ], + [ "+ a++ - ++b", "+a++-++b" ], + [ "+ a++ - --b", "+a++- --b" ], + [ "+ a++ - b", "+a++-b" ], + [ "+ a++ - b--", "+a++-b--" ], + [ "+ a++ - b++", "+a++-b++" ], + [ "+ a++ - + ++b", "+a++-+ ++b" ], + [ "+ a++ - + --b", "+a++-+--b" ], + [ "+ a++ - + b", "+a++-+b" ], + [ "+ a++ - + b--", "+a++-+b--" ], + [ "+ a++ - + b++", "+a++-+b++" ], + [ "+ a++ - - ++b", "+a++- -++b" ], + [ "+ a++ - - --b", "+a++- - --b" ], + [ "+ a++ - - b", "+a++- -b" ], + [ "+ a++ - - b--", "+a++- -b--" ], + [ "+ a++ - - b++", "+a++- -b++" ], + [ "- ++a + ++b", "-++a+ ++b" ], + [ "- ++a + --b", "-++a+--b" ], + [ "- ++a + b", "-++a+b" ], + [ "- ++a + b--", "-++a+b--" ], + [ "- ++a + b++", "-++a+b++" ], + [ "- ++a + + ++b", "-++a+ + ++b" ], + [ "- ++a + + --b", "-++a+ +--b" ], + [ "- ++a + + b", "-++a+ +b" ], + [ "- ++a + + b--", "-++a+ +b--" ], + [ "- ++a + + b++", "-++a+ +b++" ], + [ "- ++a + - ++b", "-++a+-++b" ], + [ "- ++a + - --b", "-++a+- --b" ], + [ "- ++a + - b", "-++a+-b" ], + [ "- ++a + - b--", "-++a+-b--" ], + [ "- ++a + - b++", "-++a+-b++" ], + [ "- ++a - ++b", "-++a-++b" ], + [ "- ++a - --b", "-++a- --b" ], + [ "- ++a - b", "-++a-b" ], + [ "- ++a - b--", "-++a-b--" ], + [ "- ++a - b++", "-++a-b++" ], + [ "- ++a - + ++b", "-++a-+ ++b" ], + [ "- ++a - + --b", "-++a-+--b" ], + [ "- ++a - + b", "-++a-+b" ], + [ "- ++a - + b--", "-++a-+b--" ], + [ "- ++a - + b++", "-++a-+b++" ], + [ "- ++a - - ++b", "-++a- -++b" ], + [ "- ++a - - --b", "-++a- - --b" ], + [ "- ++a - - b", "-++a- -b" ], + [ "- ++a - - b--", "-++a- -b--" ], + [ "- ++a - - b++", "-++a- -b++" ], + [ "- --a + ++b", "- --a+ ++b" ], + [ "- --a + --b", "- --a+--b" ], + [ "- --a + b", "- --a+b" ], + [ "- --a + b--", "- --a+b--" ], + [ "- --a + b++", "- --a+b++" ], + [ "- --a + + ++b", "- --a+ + ++b" ], + [ "- --a + + --b", "- --a+ +--b" ], + [ "- --a + + b", "- --a+ +b" ], + [ "- --a + + b--", "- --a+ +b--" ], + [ "- --a + + b++", "- --a+ +b++" ], + [ "- --a + - ++b", "- --a+-++b" ], + [ "- --a + - --b", "- --a+- --b" ], + [ "- --a + - b", "- --a+-b" ], + [ "- --a + - b--", "- --a+-b--" ], + [ "- --a + - b++", "- --a+-b++" ], + [ "- --a - ++b", "- --a-++b" ], + [ "- --a - --b", "- --a- --b" ], + [ "- --a - b", "- --a-b" ], + [ "- --a - b--", "- --a-b--" ], + [ "- --a - b++", "- --a-b++" ], + [ "- --a - + ++b", "- --a-+ ++b" ], + [ "- --a - + --b", "- --a-+--b" ], + [ "- --a - + b", "- --a-+b" ], + [ "- --a - + b--", "- --a-+b--" ], + [ "- --a - + b++", "- --a-+b++" ], + [ "- --a - - ++b", "- --a- -++b" ], + [ "- --a - - --b", "- --a- - --b" ], + [ "- --a - - b", "- --a- -b" ], + [ "- --a - - b--", "- --a- -b--" ], + [ "- --a - - b++", "- --a- -b++" ], + [ "- a + ++b", "-a+ ++b" ], + [ "- a + --b", "-a+--b" ], + [ "- a + b", "-a+b" ], + [ "- a + b--", "-a+b--" ], + [ "- a + b++", "-a+b++" ], + [ "- a + + ++b", "-a+ + ++b" ], + [ "- a + + --b", "-a+ +--b" ], + [ "- a + + b", "-a+ +b" ], + [ "- a + + b--", "-a+ +b--" ], + [ "- a + + b++", "-a+ +b++" ], + [ "- a + - ++b", "-a+-++b" ], + [ "- a + - --b", "-a+- --b" ], + [ "- a + - b", "-a+-b" ], + [ "- a + - b--", "-a+-b--" ], + [ "- a + - b++", "-a+-b++" ], + [ "- a - ++b", "-a-++b" ], + [ "- a - --b", "-a- --b" ], + [ "- a - b", "-a-b" ], + [ "- a - b--", "-a-b--" ], + [ "- a - b++", "-a-b++" ], + [ "- a - + ++b", "-a-+ ++b" ], + [ "- a - + --b", "-a-+--b" ], + [ "- a - + b", "-a-+b" ], + [ "- a - + b--", "-a-+b--" ], + [ "- a - + b++", "-a-+b++" ], + [ "- a - - ++b", "-a- -++b" ], + [ "- a - - --b", "-a- - --b" ], + [ "- a - - b", "-a- -b" ], + [ "- a - - b--", "-a- -b--" ], + [ "- a - - b++", "-a- -b++" ], + [ "- a-- + ++b", "-a--+ ++b" ], + [ "- a-- + --b", "-a--+--b" ], + [ "- a-- + b", "-a--+b" ], + [ "- a-- + b--", "-a--+b--" ], + [ "- a-- + b++", "-a--+b++" ], + [ "- a-- + + ++b", "-a--+ + ++b" ], + [ "- a-- + + --b", "-a--+ +--b" ], + [ "- a-- + + b", "-a--+ +b" ], + [ "- a-- + + b--", "-a--+ +b--" ], + [ "- a-- + + b++", "-a--+ +b++" ], + [ "- a-- + - ++b", "-a--+-++b" ], + [ "- a-- + - --b", "-a--+- --b" ], + [ "- a-- + - b", "-a--+-b" ], + [ "- a-- + - b--", "-a--+-b--" ], + [ "- a-- + - b++", "-a--+-b++" ], + [ "- a-- - ++b", "-a---++b" ], + [ "- a-- - --b", "-a--- --b" ], + [ "- a-- - b", "-a---b" ], + [ "- a-- - b--", "-a---b--" ], + [ "- a-- - b++", "-a---b++" ], + [ "- a-- - + ++b", "-a---+ ++b" ], + [ "- a-- - + --b", "-a---+--b" ], + [ "- a-- - + b", "-a---+b" ], + [ "- a-- - + b--", "-a---+b--" ], + [ "- a-- - + b++", "-a---+b++" ], + [ "- a-- - - ++b", "-a--- -++b" ], + [ "- a-- - - --b", "-a--- - --b" ], + [ "- a-- - - b", "-a--- -b" ], + [ "- a-- - - b--", "-a--- -b--" ], + [ "- a-- - - b++", "-a--- -b++" ], + [ "- a++ + ++b", "-a+++ ++b" ], + [ "- a++ + --b", "-a+++--b" ], + [ "- a++ + b", "-a+++b" ], + [ "- a++ + b--", "-a+++b--" ], + [ "- a++ + b++", "-a+++b++" ], + [ "- a++ + + ++b", "-a+++ + ++b" ], + [ "- a++ + + --b", "-a+++ +--b" ], + [ "- a++ + + b", "-a+++ +b" ], + [ "- a++ + + b--", "-a+++ +b--" ], + [ "- a++ + + b++", "-a+++ +b++" ], + [ "- a++ + - ++b", "-a+++-++b" ], + [ "- a++ + - --b", "-a+++- --b" ], + [ "- a++ + - b", "-a+++-b" ], + [ "- a++ + - b--", "-a+++-b--" ], + [ "- a++ + - b++", "-a+++-b++" ], + [ "- a++ - ++b", "-a++-++b" ], + [ "- a++ - --b", "-a++- --b" ], + [ "- a++ - b", "-a++-b" ], + [ "- a++ - b--", "-a++-b--" ], + [ "- a++ - b++", "-a++-b++" ], + [ "- a++ - + ++b", "-a++-+ ++b" ], + [ "- a++ - + --b", "-a++-+--b" ], + [ "- a++ - + b", "-a++-+b" ], + [ "- a++ - + b--", "-a++-+b--" ], + [ "- a++ - + b++", "-a++-+b++" ], + [ "- a++ - - ++b", "-a++- -++b" ], + [ "- a++ - - --b", "-a++- - --b" ], + [ "- a++ - - b", "-a++- -b" ], + [ "- a++ - - b--", "-a++- -b--" ], + [ "- a++ - - b++", "-a++- -b++" ], + ].forEach(function(exp) { + assert.strictEqual(UglifyJS.parse(exp[0]).print_to_string(), exp[1] + ";"); + }); + }); +}); From ec64acd2c8d8573abd5b77f8f8946767444841bb Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 19:34:54 +0800 Subject: [PATCH 23/28] introduce `unsafe_proto` - `Array.prototype.slice` => `[].slice` closes #1491 --- README.md | 3 +++ lib/compress.js | 23 +++++++++++++++++++++++ test/compress/properties.js | 16 ++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/README.md b/README.md index 1d1f2fcb..06ffa0e8 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). comparison are switching. Compression only works if both `comparisons` and `unsafe_comps` are both set to true. +- `unsafe_proto` (default: false) -- optimize expressions like + `Array.prototype.slice.call(a)` into `[].slice.call(a)` + - `conditionals` -- apply optimizations for `if`-s and conditional expressions diff --git a/lib/compress.js b/lib/compress.js index cb99a173..237af72c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -54,6 +54,7 @@ function Compressor(options, false_by_default) { drop_debugger : !false_by_default, unsafe : false, unsafe_comps : false, + unsafe_proto : false, conditionals : !false_by_default, comparisons : !false_by_default, evaluate : !false_by_default, @@ -3148,6 +3149,28 @@ merge(Compressor.prototype, { }) }).optimize(compressor); } + if (compressor.option("unsafe_proto") + && self.expression instanceof AST_Dot + && self.expression.property == "prototype") { + var exp = self.expression.expression; + if (exp instanceof AST_SymbolRef && exp.undeclared()) switch (exp.name) { + case "Array": + self.expression = make_node(AST_Array, self.expression, { + elements: [] + }); + break; + case "Object": + self.expression = make_node(AST_Object, self.expression, { + properties: [] + }); + break; + case "String": + self.expression = make_node(AST_String, self.expression, { + value: "" + }); + break; + } + } return self.evaluate(compressor)[0]; }); diff --git a/test/compress/properties.js b/test/compress/properties.js index 7ad54ebe..29bdfe2a 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -539,3 +539,19 @@ first_256_hex_chars_as_properties: { }; } } + +native_prototype: { + options = { + unsafe_proto: true, + } + input: { + Array.prototype.splice.apply(a, [1, 2, b, c]); + Object.prototype.hasOwnProperty.call(d, "foo"); + String.prototype.indexOf.call(e, "bar"); + } + expect: { + [].splice.apply(a, [1, 2, b, c]); + ({}).hasOwnProperty.call(d, "foo"); + "".indexOf.call(e, "bar"); + } +} From 8898b8a0fe87f71c0ea2d35face6dfbf11db27ec Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Sat, 18 Feb 2017 22:44:53 +0800 Subject: [PATCH 24/28] clean up `max_line_len` - never exceed specified limit - otherwise warning is shown - enabled only for final output closes #1496 --- bin/uglifyjs | 11 +++++---- lib/output.js | 43 ++++++++++++++++++++++++++--------- test/compress/max_line_len.js | 28 +++++++++++++++++++++++ tools/node.js | 2 +- 4 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 test/compress/max_line_len.js diff --git a/bin/uglifyjs b/bin/uglifyjs index 8cb2f0df..27717fb6 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -228,9 +228,10 @@ if (ARGS.mangle_props === true) { } var OUTPUT_OPTIONS = { - beautify : BEAUTIFY ? true : false, - preamble : ARGS.preamble || null, - quote_style : ARGS.quotes != null ? ARGS.quotes : 0 + beautify : BEAUTIFY ? true : false, + max_line_len : 32000, + preamble : ARGS.preamble || null, + quote_style : ARGS.quotes != null ? ARGS.quotes : 0, }; if (ARGS.mangle_props == 2) { @@ -540,7 +541,7 @@ function getOptions(flag, constants) { ast.walk(new UglifyJS.TreeWalker(function(node){ if (node instanceof UglifyJS.AST_Seq) return; // descend if (node instanceof UglifyJS.AST_Assign) { - var name = node.left.print_to_string({ beautify: false }).replace(/-/g, "_"); + var name = node.left.print_to_string().replace(/-/g, "_"); var value = node.right; if (constants) value = new Function("return (" + value.print_to_string() + ")")(); @@ -548,7 +549,7 @@ function getOptions(flag, constants) { return true; // no descend } if (node instanceof UglifyJS.AST_Symbol || node instanceof UglifyJS.AST_Binary) { - var name = node.print_to_string({ beautify: false }).replace(/-/g, "_"); + var name = node.print_to_string().replace(/-/g, "_"); ret[name] = true; return true; // no descend } diff --git a/lib/output.js b/lib/output.js index 2802c305..4a0a1e0e 100644 --- a/lib/output.js +++ b/lib/output.js @@ -70,7 +70,7 @@ function OutputStream(options) { unescape_regexps : false, inline_script : false, width : 80, - max_line_len : 32000, + max_line_len : false, beautify : false, source_map : null, bracketize : false, @@ -198,16 +198,29 @@ function OutputStream(options) { var might_need_space = false; var might_need_semicolon = false; + var might_add_newline = 0; var last = null; function last_char() { return last.charAt(last.length - 1); }; - function maybe_newline() { - if (options.max_line_len && current_col > options.max_line_len) - print("\n"); - }; + var ensure_line_len = options.max_line_len ? function() { + if (current_col > options.max_line_len) { + if (might_add_newline) { + var left = OUTPUT.slice(0, might_add_newline); + var right = OUTPUT.slice(might_add_newline); + OUTPUT = left + "\n" + right; + current_line++; + current_pos++; + current_col = right.length; + } + if (current_col > options.max_line_len) { + AST_Node.warn("Output exceeds {max_line_len} characters", options); + } + } + might_add_newline = 0; + } : noop; var requireSemicolonChars = makePredicate("( [ + * / - , ."); @@ -223,6 +236,7 @@ function OutputStream(options) { current_col++; current_pos++; } else { + ensure_line_len(); OUTPUT += "\n"; current_pos++; current_line++; @@ -243,6 +257,7 @@ function OutputStream(options) { if (!options.beautify && options.preserve_line && stack[stack.length - 1]) { var target_line = stack[stack.length - 1].start.line; while (current_line < target_line) { + ensure_line_len(); OUTPUT += "\n"; current_pos++; current_line++; @@ -264,16 +279,16 @@ function OutputStream(options) { } might_need_space = false; } + OUTPUT += str; + current_pos += str.length; var a = str.split(/\r?\n/), n = a.length - 1; current_line += n; - if (n == 0) { - current_col += a[n].length; - } else { + current_col += a[0].length; + if (n > 0) { + ensure_line_len(); current_col = a[n].length; } - current_pos += str.length; last = str; - OUTPUT += str; }; var space = options.beautify ? function() { @@ -299,7 +314,10 @@ function OutputStream(options) { var newline = options.beautify ? function() { print("\n"); - } : maybe_newline; + } : options.max_line_len ? function() { + ensure_line_len(); + might_add_newline = OUTPUT.length; + } : noop; var semicolon = options.beautify ? function() { print(";"); @@ -376,6 +394,9 @@ function OutputStream(options) { } : noop; function get() { + if (might_add_newline) { + ensure_line_len(); + } return OUTPUT; }; diff --git a/test/compress/max_line_len.js b/test/compress/max_line_len.js new file mode 100644 index 00000000..b9e09178 --- /dev/null +++ b/test/compress/max_line_len.js @@ -0,0 +1,28 @@ +too_short: { + beautify = { + max_line_len: 10, + } + input: { + function f(a) { + return { c: 42, d: a(), e: "foo"}; + } + } + expect_exact: 'function f(a){\nreturn{\nc:42,\nd:a(),\ne:"foo"}}' + expect_warnings: [ + "WARN: Output exceeds 10 characters" + ] +} + +just_enough: { + beautify = { + max_line_len: 14, + } + input: { + function f(a) { + return { c: 42, d: a(), e: "foo"}; + } + } + expect_exact: 'function f(a){\nreturn{c:42,\nd:a(),e:"foo"}\n}' + expect_warnings: [ + ] +} diff --git a/tools/node.js b/tools/node.js index c68faaa5..108803e5 100644 --- a/tools/node.js +++ b/tools/node.js @@ -115,7 +115,7 @@ exports.minify = function(files, options) { // 5. output var inMap = options.inSourceMap; - var output = {}; + var output = { max_line_len: 32000 }; if (typeof options.inSourceMap == "string") { inMap = JSON.parse(fs.readFileSync(options.inSourceMap, "utf8")); } From 26fbeece1c385a0e63efe3a6683af8459f4e495a Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Mon, 20 Feb 2017 01:46:59 +0800 Subject: [PATCH 25/28] fix `pure_funcs` & improve `side_effects` - only drops side-effect-free arguments - drop side-effect-free parts with discarded value from `AST_Seq` & `AST_SimpleStatement` closes #1494 --- lib/compress.js | 178 +++++++++++++++++++-- test/compress/drop-unused.js | 58 +++++++ test/compress/pure_funcs.js | 295 +++++++++++++++++++++++++++++++++++ 3 files changed, 516 insertions(+), 15 deletions(-) create mode 100644 test/compress/pure_funcs.js diff --git a/lib/compress.js b/lib/compress.js index 237af72c..4dfcdcfb 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -83,6 +83,14 @@ function Compressor(options, false_by_default) { global_defs : {}, passes : 1, }, true); + var pure_funcs = this.options["pure_funcs"]; + if (typeof pure_funcs == "function") { + this.pure_funcs = pure_funcs; + } else { + this.pure_funcs = pure_funcs ? function(node) { + return pure_funcs.indexOf(node.expression.print_to_string()) < 0; + } : return_true; + } var top_retain = this.options["top_retain"]; if (top_retain instanceof RegExp) { this.top_retain = function(def) { @@ -304,6 +312,13 @@ merge(Compressor.prototype, { } } + function is_iife_call(node) { + if (node instanceof AST_Call && !(node instanceof AST_New)) { + return node.expression instanceof AST_Function || is_iife_call(node.expression); + } + return false; + } + function tighten_body(statements, compressor) { var CHANGED, max_iter = 10; do { @@ -1354,10 +1369,12 @@ merge(Compressor.prototype, { def(AST_This, return_false); def(AST_Call, function(compressor){ - var pure = compressor.option("pure_funcs"); - if (!pure) return true; - if (typeof pure == "function") return pure(this); - return pure.indexOf(this.expression.print_to_string()) < 0; + if (compressor.pure_funcs(this)) return true; + for (var i = this.args.length; --i >= 0;) { + if (this.args[i].has_side_effects(compressor)) + return true; + } + return false; }); def(AST_Block, function(compressor){ @@ -1855,12 +1872,151 @@ merge(Compressor.prototype, { return self; }); + // drop_side_effect_free() + // remove side-effect-free parts which only affects return value + (function(def){ + function return_this() { + return this; + } + + function return_null() { + return null; + } + + // Drop side-effect-free elements from an array of expressions. + // Returns an array of expressions with side-effects or null + // if all elements were dropped. Note: original array may be + // returned if nothing changed. + function trim(nodes, compressor, first_in_statement) { + var ret = [], changed = false; + for (var i = 0, ii = nodes.length; i < ii; i++) { + var node = nodes[i].drop_side_effect_free(compressor, first_in_statement); + changed |= node !== nodes[i]; + if (node) { + ret.push(node); + first_in_statement = false; + } + } + return changed ? ret.length ? ret : null : nodes; + } + + def(AST_Node, return_this); + def(AST_Constant, return_null); + def(AST_This, return_null); + def(AST_Call, function(compressor, first_in_statement){ + if (compressor.pure_funcs(this)) return this; + var args = trim(this.args, compressor, first_in_statement); + return args && AST_Seq.from_array(args); + }); + def(AST_Function, return_null); + def(AST_Binary, function(compressor, first_in_statement){ + var right = this.right.drop_side_effect_free(compressor); + if (!right) return this.left.drop_side_effect_free(compressor, first_in_statement); + switch (this.operator) { + case "&&": + case "||": + var node = this.clone(); + node.right = right; + return node; + default: + var left = this.left.drop_side_effect_free(compressor, first_in_statement); + if (!left) return this.right.drop_side_effect_free(compressor, first_in_statement); + return make_node(AST_Seq, this, { + car: left, + cdr: right + }); + } + }); + def(AST_Assign, return_this); + def(AST_Conditional, function(compressor){ + var consequent = this.consequent.drop_side_effect_free(compressor); + var alternative = this.alternative.drop_side_effect_free(compressor); + if (consequent === this.consequent && alternative === this.alternative) return this; + if (!consequent) return alternative ? make_node(AST_Binary, this, { + operator: "||", + left: this.condition, + right: alternative + }) : this.condition.drop_side_effect_free(compressor); + if (!alternative) return make_node(AST_Binary, this, { + operator: "&&", + left: this.condition, + right: consequent + }); + var node = this.clone(); + node.consequent = consequent; + node.alternative = alternative; + return node; + }); + def(AST_Unary, function(compressor, first_in_statement){ + switch (this.operator) { + case "delete": + case "++": + case "--": + return this; + case "typeof": + if (this.expression instanceof AST_SymbolRef) return null; + default: + if (first_in_statement && is_iife_call(this.expression)) return this; + return this.expression.drop_side_effect_free(compressor, first_in_statement); + } + }); + def(AST_SymbolRef, function() { + return this.undeclared() ? this : null; + }); + def(AST_Object, function(compressor, first_in_statement){ + var values = trim(this.properties, compressor, first_in_statement); + return values && AST_Seq.from_array(values); + }); + def(AST_ObjectProperty, function(compressor, first_in_statement){ + return this.value.drop_side_effect_free(compressor, first_in_statement); + }); + def(AST_Array, function(compressor, first_in_statement){ + var values = trim(this.elements, compressor, first_in_statement); + return values && AST_Seq.from_array(values); + }); + def(AST_Dot, function(compressor, first_in_statement){ + if (!compressor.option("pure_getters")) return this; + return this.expression.drop_side_effect_free(compressor, first_in_statement); + }); + def(AST_Sub, function(compressor, first_in_statement){ + if (!compressor.option("pure_getters")) return this; + var expression = this.expression.drop_side_effect_free(compressor, first_in_statement); + if (!expression) return this.property.drop_side_effect_free(compressor, first_in_statement); + var property = this.property.drop_side_effect_free(compressor); + if (!property) return expression; + return make_node(AST_Seq, this, { + car: expression, + cdr: property + }); + }); + def(AST_Seq, function(compressor){ + var cdr = this.cdr.drop_side_effect_free(compressor); + if (cdr === this.cdr) return this; + if (!cdr) return this.car; + return make_node(AST_Seq, this, { + car: this.car, + cdr: cdr + }); + }); + })(function(node, func){ + node.DEFMETHOD("drop_side_effect_free", func); + }); + OPT(AST_SimpleStatement, function(self, compressor){ if (compressor.option("side_effects")) { - if (!self.body.has_side_effects(compressor)) { + var body = self.body; + if (!body.has_side_effects(compressor)) { compressor.warn("Dropping side-effect-free statement [{file}:{line},{col}]", self.start); return make_node(AST_EmptyStatement, self); } + var node = body.drop_side_effect_free(compressor, true); + if (!node) { + compressor.warn("Dropping side-effect-free statement [{file}:{line},{col}]", self.start); + return make_node(AST_EmptyStatement, self); + } + if (node !== body) { + return make_node(AST_SimpleStatement, self, { body: node }); + } } return self; }); @@ -2435,13 +2591,6 @@ merge(Compressor.prototype, { return self.negate(compressor, true); } return self; - - function is_iife_call(node) { - if (node instanceof AST_Call && !(node instanceof AST_New)) { - return node.expression instanceof AST_Function || is_iife_call(node.expression); - } - return false; - } }); OPT(AST_New, function(self, compressor){ @@ -2464,9 +2613,8 @@ merge(Compressor.prototype, { OPT(AST_Seq, function(self, compressor){ if (!compressor.option("side_effects")) return self; - if (!self.car.has_side_effects(compressor)) { - return maintain_this_binding(compressor.parent(), self, self.cdr); - } + self.car = self.car.drop_side_effect_free(compressor, first_in_statement(compressor)); + if (!self.car) return maintain_this_binding(compressor.parent(), self, self.cdr); if (compressor.option("cascade")) { if (self.car instanceof AST_Assign && !self.car.left.has_side_effects(compressor)) { diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 5a09c6cd..f5a88f21 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -590,3 +590,61 @@ drop_fnames: { } } } + +global_var: { + options = { + side_effects: true, + unused: true, + } + input: { + var a; + function foo(b) { + a; + b; + c; + typeof c === "undefined"; + c + b + a; + b && b.ar(); + return b; + } + } + expect: { + var a; + function foo(b) { + c; + c; + b && b.ar(); + return b; + } + } +} + +iife: { + options = { + side_effects: true, + unused: true, + } + input: { + function f() { + var a; + ~function() {}(b); + } + } + expect: { + function f() { + ~function() {}(b); + } + } +} + +drop_value: { + options = { + side_effects: true, + } + input: { + (1, [2, foo()], 3, {a:1, b:bar()}); + } + expect: { + foo(), bar(); + } +} diff --git a/test/compress/pure_funcs.js b/test/compress/pure_funcs.js new file mode 100644 index 00000000..3cc529a8 --- /dev/null +++ b/test/compress/pure_funcs.js @@ -0,0 +1,295 @@ +array: { + options = { + pure_funcs: [ "Math.floor" ], + side_effects: true, + } + input: { + var a; + function f(b) { + Math.floor(a / b); + Math.floor(c / b); + } + } + expect: { + var a; + function f(b) { + c; + } + } +} + +func: { + options = { + pure_funcs: function(node) { + return !~node.args[0].print_to_string().indexOf("a"); + }, + side_effects: true, + } + input: { + function f(a, b) { + Math.floor(a / b); + Math.floor(c / b); + } + } + expect: { + function f(a, b) { + Math.floor(c / b); + } + } +} + +side_effects: { + options = { + pure_funcs: [ "console.log" ], + side_effects: true, + } + input: { + function f(a, b) { + console.log(a()); + console.log(b); + } + } + expect: { + function f(a, b) { + a(); + } + } +} + +unused: { + options = { + pure_funcs: [ "pure" ], + side_effects: true, + unused: true, + } + input: { + function foo() { + var u = pure(1); + var x = pure(2); + var y = pure(x); + var z = pure(pure(side_effects())); + return pure(3); + } + } + expect: { + function foo() { + side_effects(); + return pure(3); + } + } +} + +babel: { + options = { + pure_funcs: [ "_classCallCheck" ], + side_effects: true, + unused: true, + } + input: { + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) + throw new TypeError("Cannot call a class as a function"); + } + var Foo = function Foo() { + _classCallCheck(this, Foo); + }; + } + expect: { + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) + throw new TypeError("Cannot call a class as a function"); + } + var Foo = function() { + }; + } +} + +conditional: { + options = { + pure_funcs: [ "pure" ], + side_effects: true, + } + input: { + pure(1 | a() ? 2 & b() : 7 ^ c()); + pure(1 | a() ? 2 & b() : 5); + pure(1 | a() ? 4 : 7 ^ c()); + pure(1 | a() ? 4 : 5); + pure(3 ? 2 & b() : 7 ^ c()); + pure(3 ? 2 & b() : 5); + pure(3 ? 4 : 7 ^ c()); + pure(3 ? 4 : 5); + } + expect: { + 1 | a() ? b() : c(); + 1 | a() && b(); + 1 | a() || c(); + a(); + 3 ? b() : c(); + 3 && b(); + 3 || c(); + } +} + +relational: { + options = { + pure_funcs: [ "foo" ], + side_effects :true, + } + input: { + foo() in foo(); + foo() instanceof bar(); + foo() < "bar"; + bar() > foo(); + bar() != bar(); + bar() !== "bar"; + "bar" == foo(); + "bar" === bar(); + "bar" >= "bar"; + } + expect: { + bar(); + bar(); + bar(), bar(); + bar(); + bar(); + } +} + +arithmetic: { + options = { + pure_funcs: [ "foo" ], + side_effects :true, + } + input: { + foo() + foo(); + foo() - bar(); + foo() * "bar"; + bar() / foo(); + bar() & bar(); + bar() | "bar"; + "bar" >> foo(); + "bar" << bar(); + "bar" >>> "bar"; + } + expect: { + bar(); + bar(); + bar(), bar(); + bar(); + bar(); + } +} + +boolean_and: { + options = { + pure_funcs: [ "foo" ], + side_effects :true, + } + input: { + foo() && foo(); + foo() && bar(); + foo() && "bar"; + bar() && foo(); + bar() && bar(); + bar() && "bar"; + "bar" && foo(); + "bar" && bar(); + "bar" && "bar"; + } + expect: { + foo() && bar(); + bar(); + bar() && bar(); + bar(); + "bar" && bar(); + } +} + +boolean_or: { + options = { + pure_funcs: [ "foo" ], + side_effects :true, + } + input: { + foo() || foo(); + foo() || bar(); + foo() || "bar"; + bar() || foo(); + bar() || bar(); + bar() || "bar"; + "bar" || foo(); + "bar" || bar(); + "bar" || "bar"; + } + expect: { + foo() || bar(); + bar(); + bar() || bar(); + bar(); + "bar" || bar(); + } +} + +assign: { + options = { + pure_funcs: [ "foo" ], + side_effects :true, + } + input: { + var a; + function f(b) { + a = foo(); + b *= 4 + foo(); + c >>= 0 | foo(); + } + } + expect: { + var a; + function f(b) { + a = foo(); + b *= 4 + foo(); + c >>= 0 | foo(); + } + } +} + +unary: { + options = { + pure_funcs: [ "foo" ], + side_effects :true, + } + input: { + typeof foo(); + typeof bar(); + typeof "bar"; + void foo(); + void bar(); + void "bar"; + delete a[foo()]; + delete a[bar()]; + delete a["bar"]; + a[foo()]++; + a[bar()]++; + a["bar"]++; + --a[foo()]; + --a[bar()]; + --a["bar"]; + ~foo(); + ~bar(); + ~"bar"; + } + expect: { + bar(); + bar(); + delete a[foo()]; + delete a[bar()]; + delete a["bar"]; + a[foo()]++; + a[bar()]++; + a["bar"]++; + --a[foo()]; + --a[bar()]; + --a["bar"]; + bar(); + } +} From d48a3080ac873ae531a2d87679a26c5941814843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C5=A0pan=C4=9Bl?= Date: Mon, 20 Feb 2017 17:14:53 +0800 Subject: [PATCH 26/28] Fix: AST_Accessor missing start / end tokens fixes #1492 closes #1493 --- lib/parse.js | 8 ++++++-- test/mocha/accessorTokens-1492.js | 32 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 test/mocha/accessorTokens-1492.js diff --git a/lib/parse.js b/lib/parse.js index ec82d47d..37f06df7 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1308,6 +1308,10 @@ function parse($TEXT, options) { }); }); + var create_accessor = embed_tokens(function() { + return function_(AST_Accessor); + }); + var object_ = embed_tokens(function() { expect("{"); var first = true, a = []; @@ -1324,7 +1328,7 @@ function parse($TEXT, options) { a.push(new AST_ObjectGetter({ start : start, key : as_atom_node(), - value : function_(AST_Accessor), + value : create_accessor(), end : prev() })); continue; @@ -1333,7 +1337,7 @@ function parse($TEXT, options) { a.push(new AST_ObjectSetter({ start : start, key : as_atom_node(), - value : function_(AST_Accessor), + value : create_accessor(), end : prev() })); continue; diff --git a/test/mocha/accessorTokens-1492.js b/test/mocha/accessorTokens-1492.js new file mode 100644 index 00000000..861414ee --- /dev/null +++ b/test/mocha/accessorTokens-1492.js @@ -0,0 +1,32 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("Accessor tokens", function() { + it("Should fill the token information for accessors (issue #1492)", function() { + // location 0 1 2 3 4 + // 01234567890123456789012345678901234567890123456789 + var ast = UglifyJS.parse("var obj = { get latest() { return undefined; } }"); + + // test all AST_ObjectProperty tokens are set as expected + var checkedAST_ObjectProperty = false; + var checkWalker = new UglifyJS.TreeWalker(function(node, descend) { + if (node instanceof UglifyJS.AST_ObjectProperty) { + checkedAST_ObjectProperty = true; + + assert.equal(node.start.pos, 12); + assert.equal(node.end.endpos, 46); + + assert(node.key instanceof UglifyJS.AST_SymbolRef); + assert.equal(node.key.start.pos, 16); + assert.equal(node.key.end.endpos, 22); + + assert(node.value instanceof UglifyJS.AST_Accessor); + assert.equal(node.value.start.pos, 22); + assert.equal(node.value.end.endpos, 46); + + } + }); + ast.walk(checkWalker); + assert(checkedAST_ObjectProperty, "AST_ObjectProperty not found"); + }); +}); \ No newline at end of file From 1e51586996ae4fdac68a8ea597c20ab170809c43 Mon Sep 17 00:00:00 2001 From: kzc Date: Tue, 21 Feb 2017 14:24:18 +0800 Subject: [PATCH 27/28] Support marking a call as pure A function call or IIFE with an immediately preceding comment containing `@__PURE__` or `#__PURE__` is deemed to be a side-effect-free pure function call and can potentially be dropped. Depends on `side_effects` option. `[#@]__PURE__` hint will be removed from comment when pure call is dropped. fixes #1261 closes #1448 --- lib/compress.js | 20 +++++- test/compress/issue-1261.js | 118 ++++++++++++++++++++++++++++++++++++ test/mocha/minify.js | 15 +++++ 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 test/compress/issue-1261.js diff --git a/lib/compress.js b/lib/compress.js index 4dfcdcfb..95e9c1b3 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1360,6 +1360,22 @@ merge(Compressor.prototype, { }); }); + AST_Call.DEFMETHOD("has_pure_annotation", function(compressor) { + if (!compressor.option("side_effects")) return false; + if (this.pure !== undefined) return this.pure; + var pure = false; + var comments, last_comment; + if (this.start + && (comments = this.start.comments_before) + && comments.length + && /[@#]__PURE__/.test((last_comment = comments[comments.length - 1]).value)) { + compressor.warn("Dropping __PURE__ call [{file}:{line},{col}]", this.start); + last_comment.value = last_comment.value.replace(/[@#]__PURE__/g, ' '); + pure = true; + } + return this.pure = pure; + }); + // determine if expression has side effects (function(def){ def(AST_Node, return_true); @@ -1369,7 +1385,7 @@ merge(Compressor.prototype, { def(AST_This, return_false); def(AST_Call, function(compressor){ - if (compressor.pure_funcs(this)) return true; + if (!this.has_pure_annotation(compressor) && compressor.pure_funcs(this)) return true; for (var i = this.args.length; --i >= 0;) { if (this.args[i].has_side_effects(compressor)) return true; @@ -1904,7 +1920,7 @@ merge(Compressor.prototype, { def(AST_Constant, return_null); def(AST_This, return_null); def(AST_Call, function(compressor, first_in_statement){ - if (compressor.pure_funcs(this)) return this; + if (!this.has_pure_annotation(compressor) && compressor.pure_funcs(this)) return this; var args = trim(this.args, compressor, first_in_statement); return args && AST_Seq.from_array(args); }); diff --git a/test/compress/issue-1261.js b/test/compress/issue-1261.js new file mode 100644 index 00000000..dfbe2100 --- /dev/null +++ b/test/compress/issue-1261.js @@ -0,0 +1,118 @@ +pure_function_calls: { + options = { + evaluate : true, + conditionals : true, + comparisons : true, + side_effects : true, + booleans : true, + unused : true, + if_return : true, + join_vars : true, + cascade : true, + negate_iife : true, + } + input: { + // pure top-level IIFE will be dropped + // @__PURE__ - comment + (function() { + console.log("iife0"); + })(); + + // pure top-level IIFE assigned to unreferenced var will not be dropped + var iife1 = /*@__PURE__*/(function() { + console.log("iife1"); + function iife1() {} + return iife1; + })(); + + (function(){ + // pure IIFE in function scope assigned to unreferenced var will be dropped + var iife2 = /*#__PURE__*/(function() { + console.log("iife2"); + function iife2() {} + return iife2; + })(); + })(); + + // comment #__PURE__ comment + bar(), baz(), quux(); + a.b(), /* @__PURE__ */ c.d.e(), f.g(); + } + expect: { + var iife1 = function() { + console.log("iife1"); + function iife1() {} + return iife1; + }(); + + baz(), quux(); + a.b(), f.g(); + } + expect_warnings: [ + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:17,8]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:17,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:30,37]", + "WARN: Dropping unused variable iife2 [test/compress/issue-1261.js:30,16]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:28,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:38,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:39,31]", + ] +} + +pure_function_calls_toplevel: { + options = { + evaluate : true, + conditionals : true, + comparisons : true, + side_effects : true, + booleans : true, + unused : true, + if_return : true, + join_vars : true, + cascade : true, + negate_iife : true, + toplevel : true, + } + input: { + // pure top-level IIFE will be dropped + // @__PURE__ - comment + (function() { + console.log("iife0"); + })(); + + // pure top-level IIFE assigned to unreferenced var will be dropped + var iife1 = /*@__PURE__*/(function() { + console.log("iife1"); + function iife1() {} + return iife1; + })(); + + (function(){ + // pure IIFE in function scope assigned to unreferenced var will be dropped + var iife2 = /*#__PURE__*/(function() { + console.log("iife2"); + function iife2() {} + return iife2; + })(); + })(); + + // comment #__PURE__ comment + bar(), baz(), quux(); + a.b(), /* @__PURE__ */ c.d.e(), f.g(); + } + expect: { + baz(), quux(); + a.b(), f.g(); + } + expect_warnings: [ + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:79,8]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:79,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:92,37]", + "WARN: Dropping unused variable iife2 [test/compress/issue-1261.js:92,16]", + "WARN: Dropping side-effect-free statement [test/compress/issue-1261.js:90,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:100,8]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:101,31]", + "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:84,33]", + "WARN: Dropping unused variable iife1 [test/compress/issue-1261.js:84,12]", + ] +} diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 70cf73ae..8fe1565f 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -95,4 +95,19 @@ describe("minify", function() { assert.strictEqual(code, "var a=function(n){return n};"); }); }); + + describe("#__PURE__", function() { + it("should drop #__PURE__ hint after use", function() { + var result = Uglify.minify('//@__PURE__ comment1 #__PURE__ comment2\n foo(), bar();', { + fromString: true, + output: { + comments: "all", + beautify: false, + } + }); + var code = result.code; + assert.strictEqual(code, "// comment1 comment2\nbar();"); + }); + }); + }); From 4e49302916fe395f5c63992aa28c33392208fb27 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Fri, 24 Feb 2017 01:46:57 +0800 Subject: [PATCH 28/28] enable `collapse_vars` & `reduce_vars` by default - fix corner cases in `const` optimisation - deprecate `/*@const*/` fixes #1497 closes #1498 --- README.md | 5 ++--- lib/compress.js | 12 +++++++----- lib/scope.js | 12 +----------- test/compress/const.js | 4 ++++ test/compress/dead-code.js | 10 ++++++---- test/compress/drop-unused.js | 31 +++++++++++++++++++++++++++++++ test/compress/evaluate.js | 1 + test/compress/issue-1041.js | 6 ++++-- 8 files changed, 56 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 06ffa0e8..1d0244ee 100644 --- a/README.md +++ b/README.md @@ -469,8 +469,6 @@ separate file and include it into the build. For example you can have a ```javascript const DEBUG = false; const PRODUCTION = true; -// Alternative for environments that don't support `const` -/** @const */ var STAGING = false; // etc. ``` @@ -481,7 +479,8 @@ and build your code like this: UglifyJS will notice the constants and, since they cannot be altered, it will evaluate references to them to the value itself and drop unreachable code as usual. The build will contain the `const` declarations if you use -them. If you are targeting < ES6 environments, use `/** @const */ var`. +them. If you are targeting < ES6 environments which does not support `const`, +using `var` with `reduce_vars` (enabled by default) should suffice. diff --git a/lib/compress.js b/lib/compress.js index 95e9c1b3..2bc1c5a5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -69,8 +69,8 @@ function Compressor(options, false_by_default) { hoist_vars : false, if_return : !false_by_default, join_vars : !false_by_default, - collapse_vars : false, - reduce_vars : false, + collapse_vars : !false_by_default, + reduce_vars : !false_by_default, cascade : !false_by_default, side_effects : !false_by_default, pure_getters : false, @@ -1252,7 +1252,7 @@ merge(Compressor.prototype, { this._evaluating = true; try { var d = this.definition(); - if ((d.constant || compressor.option("reduce_vars") && !d.modified) && d.init) { + if (compressor.option("reduce_vars") && !d.modified && d.init) { if (compressor.option("unsafe")) { if (!HOP(d.init, '_evaluated')) { d.init._evaluated = ev(d.init, compressor); @@ -3025,9 +3025,11 @@ merge(Compressor.prototype, { return make_node(AST_Infinity, self).transform(compressor); } } - if (compressor.option("evaluate") && !isLHS(self, compressor.parent())) { + if (compressor.option("evaluate") + && compressor.option("reduce_vars") + && !isLHS(self, compressor.parent())) { var d = self.definition(); - if (d && d.constant && d.init && d.init.is_constant(compressor)) { + if (d.constant && !d.modified && d.init && d.init.is_constant(compressor)) { var original_as_string = self.print_to_string(); var const_node = make_node_from_constant(compressor, d.init.constant_value(compressor), self); var const_node_as_string = const_node.print_to_string(); diff --git a/lib/scope.js b/lib/scope.js index b0d92d7d..29e4103a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -97,7 +97,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var scope = self.parent_scope = null; var labels = new Dictionary(); var defun = null; - var last_var_had_const_pragma = false; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; @@ -154,13 +153,10 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // later. (node.scope = defun.parent_scope).def_function(node); } - else if (node instanceof AST_Var) { - last_var_had_const_pragma = node.has_const_pragma(); - } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); - def.constant = node instanceof AST_SymbolConst || last_var_had_const_pragma; + def.constant = node instanceof AST_SymbolConst; def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { @@ -369,12 +365,6 @@ AST_Symbol.DEFMETHOD("global", function(){ return this.definition().global; }); -AST_Var.DEFMETHOD("has_const_pragma", function() { - var comments_before = this.start && this.start.comments_before; - var lastComment = comments_before && comments_before[comments_before.length - 1]; - return lastComment && /@const\b/.test(lastComment.value); -}); - AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { except : [], diff --git a/test/compress/const.js b/test/compress/const.js index dd175fcc..f1f13f49 100644 --- a/test/compress/const.js +++ b/test/compress/const.js @@ -12,6 +12,7 @@ issue_1191: { join_vars : true, sequences : false, collapse_vars : false, + reduce_vars : true, } input: { function foo(rot) { @@ -43,6 +44,7 @@ issue_1194: { join_vars : true, sequences : false, collapse_vars : false, + reduce_vars : true, } input: { function f1() {const a = "X"; return a + a;} @@ -70,6 +72,7 @@ issue_1396: { join_vars : true, sequences : false, collapse_vars : false, + reduce_vars : true, } input: { function foo(a) { @@ -140,6 +143,7 @@ regexp_literal_not_const: { join_vars : true, sequences : false, collapse_vars : false, + reduce_vars : true, } input: { (function(){ diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index c83f2040..2596e80e 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -94,7 +94,8 @@ dead_code_const_declaration: { loops : true, booleans : true, conditionals : true, - evaluate : true + evaluate : true, + reduce_vars : true, }; input: { var unused; @@ -119,7 +120,8 @@ dead_code_const_annotation: { loops : true, booleans : true, conditionals : true, - evaluate : true + evaluate : true, + reduce_vars : true, }; input: { var unused; @@ -167,7 +169,8 @@ dead_code_const_annotation_complex_scope: { loops : true, booleans : true, conditionals : true, - evaluate : true + evaluate : true, + reduce_vars : true, }; input: { var unused_var; @@ -201,6 +204,5 @@ dead_code_const_annotation_complex_scope: { var beef = 'good'; var meat = 'beef'; var pork = 'bad'; - 'good' === pork && console.log('reached, not const'); } } diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index f5a88f21..c1ca1b55 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -648,3 +648,34 @@ drop_value: { foo(), bar(); } } + +const_assign: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + function f() { + const b = 2; + return 1 + b; + } + + function g() { + const b = 2; + b = 3; + return 1 + b; + } + } + expect: { + function f() { + return 3; + } + + function g() { + const b = 2; + b = 3; + return 1 + b; + } + } +} diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index f88bc538..ae5e58d6 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -602,6 +602,7 @@ unsafe_prototype_function: { call_args: { options = { evaluate: true, + reduce_vars: true, } input: { const a = 1; diff --git a/test/compress/issue-1041.js b/test/compress/issue-1041.js index 9dd176fd..cdbc22cc 100644 --- a/test/compress/issue-1041.js +++ b/test/compress/issue-1041.js @@ -13,7 +13,8 @@ const_declaration: { const_pragma: { options = { - evaluate: true + evaluate: true, + reduce_vars: true, }; input: { @@ -27,7 +28,8 @@ const_pragma: { // for completeness' sake not_const: { options = { - evaluate: true + evaluate: true, + reduce_vars: true, }; input: {