From 81f1df14d7c931cddedad9666a7c57f4373ec918 Mon Sep 17 00:00:00 2001 From: Anat Dagan Date: Fri, 10 Feb 2017 14:13:47 +0200 Subject: [PATCH 01/37] in mangle_names there is a check that the variable name is legal and that it is not a reserved word. This should apply to propsmangle as well. --- lib/propmangle.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/propmangle.js b/lib/propmangle.js index f2777475..3c75cac9 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -149,6 +149,7 @@ function mangle_properties(ast, options) { // only function declarations after this line function can_mangle(name) { + if (!is_identifier(name)) return false; if (unmangleable.indexOf(name) >= 0) return false; if (reserved.indexOf(name) >= 0) return false; if (options.only_cache) { 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 02/37] 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 03/37] 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 04/37] 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 05/37] 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 06/37] 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 07/37] 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 08/37] 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 09/37] 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 10/37] 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 11/37] `-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 12/37] 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 13/37] 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 14/37] 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 15/37] 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 16/37] 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 17/37] 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 18/37] 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 19/37] 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 20/37] 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 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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: { From 852f78491a212ba20841847436dab93abb0a8181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C5=A0pan=C4=9Bl?= Date: Fri, 24 Feb 2017 01:51:24 +0100 Subject: [PATCH 30/37] Avoid using exports when undefined (#1471) Makes direct usage within web browser easier, even if officially unsupported. --- lib/ast.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 42506cb2..61643aed 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -81,7 +81,9 @@ function DEFNODE(type, props, methods, base) { ctor.DEFMETHOD = function(name, method) { this.prototype[name] = method; }; - exports["AST_" + type] = ctor; + if (typeof exports !== "undefined") { + exports["AST_" + type] = ctor; + } return ctor; }; From cf0951f72635f898ef9dc98ced3c73a1c488cc17 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Feb 2017 04:11:21 +0800 Subject: [PATCH 31/37] allow --in-source-map inline (#1490) - limited to one input file (or `stdin`) - only works with built-in parser fixes #520 --- bin/uglifyjs | 71 ++++++++++++++++++++++------------ test/input/issue-520/input.js | 3 ++ test/input/issue-520/output.js | 2 + test/mocha/cli.js | 48 +++++++++++++++++++++++ test/mocha/minify.js | 46 ++++++++++++++++++++++ tools/node.js | 31 ++++++++++++--- 6 files changed, 171 insertions(+), 30 deletions(-) create mode 100644 test/input/issue-520/input.js create mode 100644 test/input/issue-520/output.js diff --git a/bin/uglifyjs b/bin/uglifyjs index 27717fb6..d0c3abb2 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -282,21 +282,29 @@ if (ARGS.self) { var ORIG_MAP = ARGS.in_source_map; -if (ORIG_MAP) { +if (ORIG_MAP && ORIG_MAP != "inline") { ORIG_MAP = JSON.parse(fs.readFileSync(ORIG_MAP)); if (files.length == 0) { print_error("INFO: Using file from the input source map: " + ORIG_MAP.file); files = [ ORIG_MAP.file ]; } - if (ARGS.source_map_root == null) { - ARGS.source_map_root = ORIG_MAP.sourceRoot; - } } if (files.length == 0) { files = [ "-" ]; } +if (ORIG_MAP == "inline") { + if (files.length > 1) { + print_error("ERROR: Inline source map only works with singular input"); + process.exit(1); + } + if (ARGS.acorn || ARGS.spidermonkey) { + print_error("ERROR: Inline source map only works with built-in parser"); + process.exit(1); + } +} + if (files.indexOf("-") >= 0 && ARGS.source_map) { print_error("ERROR: Source map doesn't work with input from STDIN"); process.exit(1); @@ -308,37 +316,19 @@ if (files.filter(function(el){ return el == "-" }).length > 1) { } var STATS = {}; -var OUTPUT_FILE = ARGS.o; var TOPLEVEL = null; var P_RELATIVE = ARGS.p && ARGS.p == "relative"; var SOURCES_CONTENT = {}; -var SOURCE_MAP = (ARGS.source_map || ARGS.source_map_inline) ? UglifyJS.SourceMap({ - file: P_RELATIVE ? path.relative(path.dirname(ARGS.source_map), OUTPUT_FILE) : OUTPUT_FILE, - root: ARGS.source_map_root, - orig: ORIG_MAP, -}) : null; - -OUTPUT_OPTIONS.source_map = SOURCE_MAP; - -try { - var output = UglifyJS.OutputStream(OUTPUT_OPTIONS); - var compressor = COMPRESS && UglifyJS.Compressor(COMPRESS); -} catch(ex) { - if (ex instanceof UglifyJS.DefaultsError) { - print_error(ex.msg); - print_error("Supported options:"); - print_error(sys.inspect(ex.defs)); - process.exit(1); - } -} - async.eachLimit(files, 1, function (file, cb) { read_whole_file(file, function (err, code) { if (err) { print_error("ERROR: can't read file: " + file); process.exit(1); } + if (ORIG_MAP == "inline") { + ORIG_MAP = read_source_map(code); + } if (ARGS.p != null) { if (P_RELATIVE) { file = path.relative(path.dirname(ARGS.source_map), file).replace(/\\/g, '/'); @@ -385,6 +375,28 @@ async.eachLimit(files, 1, function (file, cb) { cb(); }); }, function () { + var OUTPUT_FILE = ARGS.o; + + var SOURCE_MAP = (ARGS.source_map || ARGS.source_map_inline) ? UglifyJS.SourceMap({ + file: P_RELATIVE ? path.relative(path.dirname(ARGS.source_map), OUTPUT_FILE) : OUTPUT_FILE, + root: ARGS.source_map_root || ORIG_MAP && ORIG_MAP.sourceRoot, + orig: ORIG_MAP, + }) : null; + + OUTPUT_OPTIONS.source_map = SOURCE_MAP; + + try { + var output = UglifyJS.OutputStream(OUTPUT_OPTIONS); + var compressor = COMPRESS && UglifyJS.Compressor(COMPRESS); + } catch(ex) { + if (ex instanceof UglifyJS.DefaultsError) { + print_error(ex.msg); + print_error("Supported options:"); + print_error(sys.inspect(ex.defs)); + process.exit(1); + } + } + if (ARGS.acorn || ARGS.spidermonkey) time_it("convert_ast", function(){ TOPLEVEL = UglifyJS.AST_Node.from_mozilla_ast(TOPLEVEL); }); @@ -576,6 +588,15 @@ function read_whole_file(filename, cb) { } } +function read_source_map(code) { + var match = /\n\/\/# sourceMappingURL=data:application\/json(;.*?)?;base64,(.*)/.exec(code); + if (!match) { + print_error("WARN: inline source map not found"); + return null; + } + return JSON.parse(new Buffer(match[2], "base64")); +} + function time_it(name, cont) { var t1 = new Date().getTime(); var ret = cont(); diff --git a/test/input/issue-520/input.js b/test/input/issue-520/input.js new file mode 100644 index 00000000..62428a52 --- /dev/null +++ b/test/input/issue-520/input.js @@ -0,0 +1,3 @@ +var Foo = function Foo(){console.log(1+2);}; new Foo(); + +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjpudWxsLCJzb3VyY2VzIjpbInN0ZGluIl0sInNvdXJjZXNDb250ZW50IjpbImNsYXNzIEZvbyB7IGNvbnN0cnVjdG9yKCl7Y29uc29sZS5sb2coMSsyKTt9IH0gbmV3IEZvbygpO1xuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLElBQU0sR0FBRyxHQUFDLEFBQUUsWUFBVyxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFBLEFBQUUsQ0FBQyxJQUFJLEdBQUcsRUFBRSxDQUFDOyJ9 diff --git a/test/input/issue-520/output.js b/test/input/issue-520/output.js new file mode 100644 index 00000000..8d19855b --- /dev/null +++ b/test/input/issue-520/output.js @@ -0,0 +1,2 @@ +new function(){console.log(3)}; +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0ZGluIl0sIm5hbWVzIjpbImNvbnNvbGUiLCJsb2ciXSwibWFwcGluZ3MiOiJBQUErQyxHQUFyQyxZQUFnQkEsUUFBUUMsSUFBSSIsInNvdXJjZXNDb250ZW50IjpbImNsYXNzIEZvbyB7IGNvbnN0cnVjdG9yKCl7Y29uc29sZS5sb2coMSsyKTt9IH0gbmV3IEZvbygpO1xuIl19 diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 450df1fd..52c70935 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -151,4 +151,52 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should process inline source map", function(done) { + var command = uglifyjscmd + ' test/input/issue-520/input.js -cm toplevel --in-source-map inline --source-map-inline'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, readFileSync("test/input/issue-520/output.js", "utf8")); + done(); + }); + }); + it("Should warn for missing inline source map", function(done) { + var command = uglifyjscmd + ' test/input/issue-1323/sample.js --in-source-map inline'; + + exec(command, function (err, stdout, stderr) { + if (err) throw err; + + assert.strictEqual(stdout, "var bar=function(){function foo(bar){return bar}return foo}();\n"); + assert.strictEqual(stderr, "WARN: inline source map not found\n"); + done(); + }); + }); + it("Should fail with multiple input and inline source map", function(done) { + var command = uglifyjscmd + ' test/input/issue-520/input.js test/input/issue-520/output.js --in-source-map inline --source-map-inline'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stderr, "ERROR: Inline source map only works with singular input\n"); + done(); + }); + }); + it("Should fail with acorn and inline source map", function(done) { + var command = uglifyjscmd + ' test/input/issue-520/input.js --in-source-map inline --source-map-inline --acorn'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stderr, "ERROR: Inline source map only works with built-in parser\n"); + done(); + }); + }); + it("Should fail with SpiderMonkey and inline source map", function(done) { + var command = uglifyjscmd + ' test/input/issue-520/input.js --in-source-map inline --source-map-inline --spidermonkey'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stderr, "ERROR: Inline source map only works with built-in parser\n"); + done(); + }); + }); }); diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 8fe1565f..1b830cb5 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -1,5 +1,6 @@ var Uglify = require('../../'); var assert = require("assert"); +var readFileSync = require("fs").readFileSync; describe("minify", function() { it("Should test basic sanity of minify with default options", function() { @@ -75,6 +76,51 @@ describe("minify", function() { assert.equal(map.sourcesContent[0], 'let foo = x => "foo " + x;\nconsole.log(foo("bar"));'); }); + it("Should process inline source map", function() { + var code = Uglify.minify("./test/input/issue-520/input.js", { + inSourceMap: "inline", + sourceMapInline: true + }).code + "\n"; + assert.strictEqual(code, readFileSync("test/input/issue-520/output.js", "utf8")); + }); + it("Should warn for missing inline source map", function() { + var warn_function = Uglify.AST_Node.warn_function; + var warnings = []; + Uglify.AST_Node.warn_function = function(txt) { + warnings.push(txt); + }; + try { + var result = Uglify.minify("./test/input/issue-1323/sample.js", { + inSourceMap: "inline", + mangle: false, + }); + assert.strictEqual(result.code, "var bar=function(){function foo(bar){return bar}return foo}();"); + assert.strictEqual(warnings.length, 1); + assert.strictEqual(warnings[0], "inline source map not found"); + } finally { + Uglify.AST_Node.warn_function = warn_function; + } + }); + it("Should fail with multiple input and inline source map", function() { + assert.throws(function() { + Uglify.minify([ + "./test/input/issue-520/input.js", + "./test/input/issue-520/output.js" + ], { + inSourceMap: "inline", + sourceMapInline: true + }); + }, "multiple input and inline source map"); + }); + it("Should fail with SpiderMonkey and inline source map", function() { + assert.throws(function() { + Uglify.minify("./test/input/issue-520/input.js", { + inSourceMap: "inline", + sourceMapInline: true, + spidermonkey: true + }); + }, "SpiderMonkey and inline source map"); + }); }); describe("sourceMapInline", function() { diff --git a/tools/node.js b/tools/node.js index 108803e5..c64b4e5c 100644 --- a/tools/node.js +++ b/tools/node.js @@ -37,6 +37,15 @@ UglifyJS.AST_Node.warn_function = function(txt) { console.error("WARN: %s", txt); }; +function read_source_map(code) { + var match = /\n\/\/# sourceMappingURL=data:application\/json(;.*?)?;base64,(.*)/.exec(code); + if (!match) { + UglifyJS.AST_Node.warn("inline source map not found"); + return null; + } + return JSON.parse(new Buffer(match[2], "base64")); +} + exports.minify = function(files, options) { options = UglifyJS.defaults(options, { spidermonkey : false, @@ -57,17 +66,28 @@ exports.minify = function(files, options) { }); UglifyJS.base54.reset(); + var inMap = options.inSourceMap; + if (typeof inMap == "string" && inMap != "inline") { + inMap = JSON.parse(fs.readFileSync(inMap, "utf8")); + } + // 1. parse var toplevel = null, sourcesContent = {}; if (options.spidermonkey) { + if (inMap == "inline") { + throw new Error("inline source map only works with built-in parser"); + } toplevel = UglifyJS.AST_Node.from_mozilla_ast(files); } else { function addFile(file, fileUrl) { var code = options.fromString ? file : fs.readFileSync(file, "utf8"); + if (inMap == "inline") { + inMap = read_source_map(code); + } sourcesContent[fileUrl] = code; toplevel = UglifyJS.parse(code, { filename: fileUrl, @@ -75,7 +95,12 @@ exports.minify = function(files, options) { bare_returns: options.parse ? options.parse.bare_returns : undefined }); } - if (!options.fromString) files = UglifyJS.simple_glob(files); + if (!options.fromString) { + files = UglifyJS.simple_glob(files); + if (inMap == "inline" && files.length > 1) { + throw new Error("inline source map only works with singular input"); + } + } [].concat(files).forEach(function (files, i) { if (typeof files === 'string') { addFile(files, options.fromString ? i : files); @@ -114,11 +139,7 @@ exports.minify = function(files, options) { } // 5. output - var inMap = options.inSourceMap; var output = { max_line_len: 32000 }; - if (typeof options.inSourceMap == "string") { - inMap = JSON.parse(fs.readFileSync(options.inSourceMap, "utf8")); - } if (options.outSourceMap || options.sourceMapInline) { output.source_map = UglifyJS.SourceMap({ // prefer outFileName, otherwise use outSourceMap without .map suffix From 834f9f39245f1ddf5a29c579afd7ebcdb0afe585 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Feb 2017 04:13:10 +0800 Subject: [PATCH 32/37] update docs for `pure_funcs` & `drop_console` (#1503) closes #1362 closes #1399 --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1d0244ee..490f178c 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,9 @@ The available options are: -b, --beautify Beautify output/specify output options. -m, --mangle Mangle names/pass mangler options. -r, --reserved Reserved names to exclude from mangling. - -c, --compress Enable compressor/pass compressor options. Pass - options like -c - hoist_vars=false,if_return=false. Use -c with - no argument to use the default compression + -c, --compress Enable compressor/pass compressor options, e.g. + `-c 'if_return=false,pure_funcs=["Math.pow","console.log"]'` + Use `-c` with no argument to enable default compression options. -d, --define Global definitions -e, --enclose Embed everything in a big function, with a @@ -151,8 +150,10 @@ The available options are: them explicitly on the command line. --mangle-regex Only mangle property names matching the regex --name-cache File to hold mangled names mappings - --pure-funcs List of functions that can be safely removed if - their return value is not used [array] + --pure-funcs Functions that can be safely removed if their + return value is not used, e.g. + `--pure-funcs Math.floor console.info` + (requires `--compress`) ``` Specify `--output` (`-o`) to declare the output file. Otherwise the output @@ -415,7 +416,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). overhead (compression will be slower). - `drop_console` -- default `false`. Pass `true` to discard calls to - `console.*` functions. + `console.*` functions. If you wish to drop a specific function call + such as `console.info` and/or retain side effects from function arguments + after dropping the function call then use `pure_funcs` instead. - `keep_fargs` -- default `true`. Prevents the compressor from discarding unused function arguments. You need this From 16cd5d57a5cf7f5750104df0e5af246708fd493f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Feb 2017 00:40:33 +0800 Subject: [PATCH 33/37] consolidate `evaluate` & `reduce_vars` (#1505) - improve marking efficiency - apply smarter `const` replacement to `var` fixes #1501 --- lib/compress.js | 81 +++++++++++++++------------ lib/scope.js | 2 - test/compress/evaluate.js | 2 +- test/compress/reduce_vars.js | 16 +++--- test/mocha/comment_before_constant.js | 6 +- 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 2bc1c5a5..ccd6e23c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -118,7 +118,7 @@ merge(Compressor.prototype, { var passes = +this.options.passes || 1; for (var pass = 0; pass < passes && pass < 3; ++pass) { if (pass > 0 || this.option("reduce_vars")) - node.reset_opt_flags(this); + node.reset_opt_flags(this, true); node = node.transform(this); } return node; @@ -177,28 +177,26 @@ merge(Compressor.prototype, { return this.print_to_string() == node.print_to_string(); }); - AST_Node.DEFMETHOD("reset_opt_flags", function(compressor){ - var reduce_vars = compressor.option("reduce_vars"); + AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan){ + var reduce_vars = rescan && compressor.option("reduce_vars"); + var unsafe = compressor.option("unsafe"); 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.init) { - delete d.init._evaluated; + if (reduce_vars) { + if (node instanceof AST_Toplevel) node.globals.each(reset_def); + if (node instanceof AST_Scope) node.variables.each(reset_def); + if (node instanceof AST_SymbolRef) { + var d = node.definition(); + d.references.push(node); + if (!d.modified && (d.orig.length > 1 || isModified(node, 0))) { + d.modified = true; + } } - if (reduce_vars && (d.orig.length > 1 || isModified(node, 0))) { - d.modified = true; + if (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 (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; @@ -206,10 +204,18 @@ merge(Compressor.prototype, { }); this.walk(tw); + function reset_def(def) { + def.modified = false; + def.references = []; + def.should_replace = undefined; + if (unsafe && def.init) { + def.init._evaluated = undefined; + } + } + 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 + if (isLHS(node, parent) || parent instanceof AST_Call && parent.expression === node) { return true; } else if (parent instanceof AST_PropAccess && parent.expression === node) { @@ -1254,7 +1260,7 @@ merge(Compressor.prototype, { var d = this.definition(); if (compressor.option("reduce_vars") && !d.modified && d.init) { if (compressor.option("unsafe")) { - if (!HOP(d.init, '_evaluated')) { + if (d.init._evaluated === undefined) { d.init._evaluated = ev(d.init, compressor); } return d.init._evaluated; @@ -2403,9 +2409,6 @@ 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()) { @@ -3025,18 +3028,24 @@ merge(Compressor.prototype, { return make_node(AST_Infinity, self).transform(compressor); } } - if (compressor.option("evaluate") - && compressor.option("reduce_vars") - && !isLHS(self, compressor.parent())) { + if (compressor.option("evaluate") && compressor.option("reduce_vars")) { var d = self.definition(); - 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(); - 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; + if (!d.modified && d.init) { + if (d.should_replace === undefined) { + var init = d.init.evaluate(compressor); + if (init.length > 1) { + var value = init[0].print_to_string().length; + var name = d.name.length; + var freq = d.references.length; + var overhead = d.global || !freq ? 0 : (name + 2 + value) / freq; + d.should_replace = value <= name + overhead ? init[0] : false; + } else { + d.should_replace = false; + } + } + if (d.should_replace) { + return d.should_replace; + } } } return self; diff --git a/lib/scope.js b/lib/scope.js index 29e4103a..ae0c5777 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -51,7 +51,6 @@ function SymbolDef(scope, index, orig) { this.global = false; this.mangled_name = null; this.undeclared = false; - this.constant = false; this.index = index; this.id = SymbolDef.next_id++; }; @@ -156,7 +155,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); - def.constant = node instanceof AST_SymbolConst; def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index ae5e58d6..5cefadc8 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -615,7 +615,7 @@ call_args: { const a = 1; console.log(1); +function(a) { - return a; + return 1; }(1); } } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index d9d02efa..0ee201c0 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -134,8 +134,8 @@ modified: { } function f2() { - var b = 2, c = 3; - b = c; + var b = 2; + b = 3; console.log(1 + b); console.log(b + 3); console.log(4); @@ -143,8 +143,8 @@ modified: { } function f3() { - var b = 2, c = 3; - b *= c; + var b = 2; + b *= 3; console.log(1 + b); console.log(b + 3); console.log(4); @@ -236,7 +236,7 @@ unsafe_evaluate_object: { function f0(){ var a = 1; var b = {}; - b[a] = 2; + b[1] = 2; console.log(4); } @@ -280,7 +280,7 @@ unsafe_evaluate_array: { function f0(){ var a = 1; var b = []; - b[a] = 2; + b[1] = 2; console.log(4); } @@ -373,8 +373,8 @@ passes: { } expect: { function f() { - var b = 2, c = 3; - b = c; + var b = 2; + b = 3; console.log(1 + b); console.log(b + 3); console.log(4); diff --git a/test/mocha/comment_before_constant.js b/test/mocha/comment_before_constant.js index cfdb6da1..eaa8691c 100644 --- a/test/mocha/comment_before_constant.js +++ b/test/mocha/comment_before_constant.js @@ -7,7 +7,7 @@ describe("comment before constant", function() { it("Should test comment before constant is retained and output after mangle.", function() { var result = Uglify.minify(js, { fromString: true, - compress: { collapse_vars: false }, + compress: { collapse_vars: false, reduce_vars: false }, mangle: {}, output: { comments: true }, }); @@ -17,9 +17,9 @@ describe("comment before constant", function() { it("Should test code works when comments disabled.", function() { var result = Uglify.minify(js, { fromString: true, - compress: { collapse_vars: false }, + compress: { collapse_vars: false, reduce_vars: false }, mangle: {}, - output: {}, + output: { comments: false }, }); assert.strictEqual(result.code, 'function f(){var n=!1;return n}'); }); From 13be50a4a9e34d65fef834625f44545e849fed02 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Feb 2017 05:58:26 +0800 Subject: [PATCH 34/37] faster tree transversal (#1462) - convert `[].forEach()` to for-loops --- lib/ast.js | 46 ++++++++++++++++++++++++++-------------------- lib/compress.js | 2 +- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 61643aed..f3df78fe 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -145,12 +145,13 @@ var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", { }, AST_Statement); function walk_body(node, visitor) { - if (node.body instanceof AST_Statement) { - node.body._walk(visitor); + var body = node.body; + if (body instanceof AST_Statement) { + body._walk(visitor); + } + else for (var i = 0, len = body.length; i < len; i++) { + body[i]._walk(visitor); } - else node.body.forEach(function(stat){ - stat._walk(visitor); - }); }; var AST_Block = DEFNODE("Block", "body", { @@ -371,9 +372,10 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { _walk: function(visitor) { return visitor._visit(this, function(){ if (this.name) this.name._walk(visitor); - this.argnames.forEach(function(arg){ - arg._walk(visitor); - }); + var argnames = this.argnames; + for (var i = 0, len = argnames.length; i < len; i++) { + argnames[i]._walk(visitor); + } walk_body(this, visitor); }); } @@ -533,9 +535,10 @@ var AST_Definitions = DEFNODE("Definitions", "definitions", { }, _walk: function(visitor) { return visitor._visit(this, function(){ - this.definitions.forEach(function(def){ - def._walk(visitor); - }); + var definitions = this.definitions; + for (var i = 0, len = definitions.length; i < len; i++) { + definitions[i]._walk(visitor); + } }); } }, AST_Statement); @@ -573,9 +576,10 @@ var AST_Call = DEFNODE("Call", "expression args", { _walk: function(visitor) { return visitor._visit(this, function(){ this.expression._walk(visitor); - this.args.forEach(function(arg){ - arg._walk(visitor); - }); + var args = this.args; + for (var i = 0, len = args.length; i < len; i++) { + args[i]._walk(visitor); + } }); } }); @@ -742,9 +746,10 @@ var AST_Array = DEFNODE("Array", "elements", { }, _walk: function(visitor) { return visitor._visit(this, function(){ - this.elements.forEach(function(el){ - el._walk(visitor); - }); + var elements = this.elements; + for (var i = 0, len = elements.length; i < len; i++) { + elements[i]._walk(visitor); + } }); } }); @@ -756,9 +761,10 @@ var AST_Object = DEFNODE("Object", "properties", { }, _walk: function(visitor) { return visitor._visit(this, function(){ - this.properties.forEach(function(prop){ - prop._walk(visitor); - }); + var properties = this.properties; + for (var i = 0, len = properties.length; i < len; i++) { + properties[i]._walk(visitor); + } }); } }); diff --git a/lib/compress.js b/lib/compress.js index ccd6e23c..53833115 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1911,7 +1911,7 @@ merge(Compressor.prototype, { // 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++) { + for (var i = 0, len = nodes.length; i < len; i++) { var node = nodes[i].drop_side_effect_free(compressor, first_in_statement); changed |= node !== nodes[i]; if (node) { From b1c593a0419bf5ef0c6849262cb23bb34c97ea80 Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 26 Feb 2017 12:55:24 -0500 Subject: [PATCH 35/37] add harmony branch details in README (#1507) --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 490f178c..260a685d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ There's also an [in-browser online demo](http://lisperator.net/uglifyjs/#demo) (for Firefox, Chrome and probably Safari). +Note: release versions of `uglify-js` only support ECMAScript 5 (ES5). If you wish to minify +ES2015+ (ES6+) code then please use the [harmony](#harmony) development branch. + Install ------- @@ -966,3 +969,20 @@ The `source_map_options` (optional) can contain the following properties: [codegen]: http://lisperator.net/uglifyjs/codegen [compressor]: http://lisperator.net/uglifyjs/compress [parser]: http://lisperator.net/uglifyjs/parser + +#### Harmony + +If you wish to use the experimental [harmony](https://github.com/mishoo/UglifyJS2/commits/harmony) +branch to minify ES2015+ (ES6+) code please use the following in your `package.json` file: + +``` +"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony" +``` + +or to directly install the experimental harmony version of uglify: + +``` +npm install --save-dev uglify-js@github:mishoo/UglifyJS2#harmony +``` + +See [#448](https://github.com/mishoo/UglifyJS2/issues/448) for additional details. From 872270b14986b2f24df406425eda5a3bf7a3b56a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 27 Feb 2017 03:40:54 +0800 Subject: [PATCH 36/37] improve error messages (#1506) - better inheritance of `Error` sub-classes - mark parse error against source in CLI closes #235 closes #348 closes #524 closes #1356 closes #1405 --- bin/uglifyjs | 18 ++++++++-- lib/parse.js | 65 ++++++++++++++++++------------------ lib/utils.js | 19 +++++++++-- test/input/invalid/eof.js | 1 + test/input/invalid/simple.js | 1 + test/input/invalid/tab.js | 1 + test/mocha/cli.js | 39 ++++++++++++++++++++++ test/mocha/comment.js | 4 +-- test/mocha/directives.js | 2 +- test/mocha/getter-setter.js | 2 +- test/mocha/line-endings.js | 2 +- test/mocha/minify.js | 19 +++++++++-- test/mocha/number-literal.js | 2 +- test/mocha/string-literal.js | 4 +-- test/mocha/with.js | 2 +- 15 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 test/input/invalid/eof.js create mode 100644 test/input/invalid/simple.js create mode 100644 test/input/invalid/tab.js diff --git a/bin/uglifyjs b/bin/uglifyjs index d0c3abb2..367d66e2 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -364,7 +364,21 @@ async.eachLimit(files, 1, function (file, cb) { } catch(ex) { if (ex instanceof UglifyJS.JS_Parse_Error) { print_error("Parse error at " + file + ":" + ex.line + "," + ex.col); - print_error(ex.message); + var col = ex.col; + var line = code.split(/\r?\n/)[ex.line - (col ? 1 : 2)]; + if (line) { + if (col > 40) { + line = line.slice(col - 40); + col = 40; + } + if (col) { + print_error(line.slice(0, 80)); + print_error(line.slice(0, col).replace(/\S/g, " ") + "^"); + } else { + print_error(line.slice(-40)); + print_error(line.slice(-40).replace(/\S/g, " ") + "^"); + } + } print_error(ex.stack); process.exit(1); } @@ -390,7 +404,7 @@ async.eachLimit(files, 1, function (file, cb) { var compressor = COMPRESS && UglifyJS.Compressor(COMPRESS); } catch(ex) { if (ex instanceof UglifyJS.DefaultsError) { - print_error(ex.msg); + print_error(ex.message); print_error("Supported options:"); print_error(sys.inspect(ex.defs)); process.exit(1); diff --git a/lib/parse.js b/lib/parse.js index 37f06df7..9b198ccd 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -195,12 +195,11 @@ function JS_Parse_Error(message, filename, line, col, pos) { this.line = line; this.col = col; this.pos = pos; - this.stack = new Error().stack; -}; - -JS_Parse_Error.prototype.toString = function() { - return this.message + " (line: " + this.line + ", col: " + this.col + ", pos: " + this.pos + ")" + "\n\n" + this.stack; }; +JS_Parse_Error.prototype = Object.create(Error.prototype); +JS_Parse_Error.prototype.constructor = JS_Parse_Error; +JS_Parse_Error.prototype.name = "SyntaxError"; +configure_error_stack(JS_Parse_Error); function js_error(message, filename, line, col, pos) { throw new JS_Parse_Error(message, filename, line, col, pos); @@ -350,13 +349,13 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { }); if (prefix) num = prefix + num; if (RE_OCT_NUMBER.test(num) && next_token.has_directive("use strict")) { - parse_error("SyntaxError: Legacy octal literals are not allowed in strict mode"); + parse_error("Legacy octal literals are not allowed in strict mode"); } var valid = parse_js_number(num); if (!isNaN(valid)) { return token("num", valid); } else { - parse_error("SyntaxError: Invalid syntax: " + num); + parse_error("Invalid syntax: " + num); } }; @@ -395,7 +394,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { // Parse if (ch === "0") return "\0"; if (ch.length > 0 && next_token.has_directive("use strict")) - parse_error("SyntaxError: Legacy octal escape sequences are not allowed in strict mode"); + parse_error("Legacy octal escape sequences are not allowed in strict mode"); return String.fromCharCode(parseInt(ch, 8)); } @@ -404,18 +403,18 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { for (; n > 0; --n) { var digit = parseInt(next(true), 16); if (isNaN(digit)) - parse_error("SyntaxError: Invalid hex-character pattern in string"); + parse_error("Invalid hex-character pattern in string"); num = (num << 4) | digit; } return num; }; - var read_string = with_eof_error("SyntaxError: Unterminated string constant", function(quote_char){ + var read_string = with_eof_error("Unterminated string constant", function(quote_char){ var quote = next(), ret = ""; for (;;) { var ch = next(true, true); if (ch == "\\") ch = read_escaped_char(true); - else if (NEWLINE_CHARS(ch)) parse_error("SyntaxError: Unterminated string constant"); + else if (NEWLINE_CHARS(ch)) parse_error("Unterminated string constant"); else if (ch == quote) break; ret += ch; } @@ -440,7 +439,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { return next_token; }; - var skip_multiline_comment = with_eof_error("SyntaxError: Unterminated multiline comment", function(){ + var skip_multiline_comment = with_eof_error("Unterminated multiline comment", function(){ var regex_allowed = S.regex_allowed; var i = find("*/", true); var text = S.text.substring(S.pos, i).replace(/\r\n|\r|\u2028|\u2029/g, '\n'); @@ -460,9 +459,9 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { else break; } else { - if (ch != "u") parse_error("SyntaxError: Expecting UnicodeEscapeSequence -- uXXXX"); + if (ch != "u") parse_error("Expecting UnicodeEscapeSequence -- uXXXX"); ch = read_escaped_char(); - if (!is_identifier_char(ch)) parse_error("SyntaxError: Unicode char: " + ch.charCodeAt(0) + " is not valid in identifier"); + if (!is_identifier_char(ch)) parse_error("Unicode char: " + ch.charCodeAt(0) + " is not valid in identifier"); name += ch; backslash = false; } @@ -474,10 +473,10 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { return name; }; - var read_regexp = with_eof_error("SyntaxError: Unterminated regular expression", function(regexp){ + var read_regexp = with_eof_error("Unterminated regular expression", function(regexp){ var prev_backslash = false, ch, in_class = false; while ((ch = next(true))) if (NEWLINE_CHARS(ch)) { - parse_error("SyntaxError: Unexpected line terminator"); + parse_error("Unexpected line terminator"); } else if (prev_backslash) { regexp += "\\" + ch; prev_backslash = false; @@ -498,7 +497,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { try { return token("regexp", new RegExp(regexp, mods)); } catch(e) { - parse_error("SyntaxError: " + e.message); + parse_error(e.message); } }); @@ -599,7 +598,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { } break; } - parse_error("SyntaxError: Unexpected character '" + ch + "'"); + parse_error("Unexpected character '" + ch + "'"); }; next_token.context = function(nc) { @@ -756,14 +755,14 @@ function parse($TEXT, options) { function unexpected(token) { if (token == null) token = S.token; - token_error(token, "SyntaxError: Unexpected token: " + token.type + " (" + token.value + ")"); + token_error(token, "Unexpected token: " + token.type + " (" + token.value + ")"); }; function expect_token(type, val) { if (is(type, val)) { return next(); } - token_error(S.token, "SyntaxError: Unexpected token " + S.token.type + " «" + S.token.value + "»" + ", expected " + type + " «" + val + "»"); + token_error(S.token, "Unexpected token " + S.token.type + " «" + S.token.value + "»" + ", expected " + type + " «" + val + "»"); }; function expect(punc) { return expect_token("punc", punc); }; @@ -892,7 +891,7 @@ function parse($TEXT, options) { case "return": if (S.in_function == 0 && !options.bare_returns) - croak("SyntaxError: 'return' outside of function"); + croak("'return' outside of function"); return new AST_Return({ value: ( is("punc", ";") ? (next(), null) @@ -909,7 +908,7 @@ function parse($TEXT, options) { case "throw": if (S.token.nlb) - croak("SyntaxError: Illegal newline after 'throw'"); + croak("Illegal newline after 'throw'"); return new AST_Throw({ value: (tmp = expression(true), semicolon(), tmp) }); @@ -925,7 +924,7 @@ function parse($TEXT, options) { case "with": if (S.input.has_directive("use strict")) { - croak("SyntaxError: Strict mode may not include a with statement"); + croak("Strict mode may not include a with statement"); } return new AST_With({ expression : parenthesised(), @@ -945,7 +944,7 @@ function parse($TEXT, options) { // syntactically incorrect if it contains a // LabelledStatement that is enclosed by a // LabelledStatement with the same Identifier as label. - croak("SyntaxError: Label " + label.name + " defined twice"); + croak("Label " + label.name + " defined twice"); } expect(":"); S.labels.push(label); @@ -958,7 +957,7 @@ function parse($TEXT, options) { label.references.forEach(function(ref){ if (ref instanceof AST_Continue) { ref = ref.label.start; - croak("SyntaxError: Continue label `" + label.name + "` refers to non-IterationStatement.", + croak("Continue label `" + label.name + "` refers to non-IterationStatement.", ref.line, ref.col, ref.pos); } }); @@ -978,11 +977,11 @@ function parse($TEXT, options) { if (label != null) { ldef = find_if(function(l){ return l.name == label.name }, S.labels); if (!ldef) - croak("SyntaxError: Undefined label " + label.name); + croak("Undefined label " + label.name); label.thedef = ldef; } else if (S.in_loop == 0) - croak("SyntaxError: " + type.TYPE + " not inside a loop or switch"); + croak(type.TYPE + " not inside a loop or switch"); semicolon(); var stat = new type({ label: label }); if (ldef) ldef.references.push(stat); @@ -998,7 +997,7 @@ function parse($TEXT, options) { : expression(true, true); if (is("operator", "in")) { if (init instanceof AST_Var && init.definitions.length > 1) - croak("SyntaxError: Only one variable declaration allowed in for..in loop"); + croak("Only one variable declaration allowed in for..in loop"); next(); return for_in(init); } @@ -1148,7 +1147,7 @@ function parse($TEXT, options) { }); } if (!bcatch && !bfinally) - croak("SyntaxError: Missing catch/finally blocks"); + croak("Missing catch/finally blocks"); return new AST_Try({ body : body, bcatch : bcatch, @@ -1242,7 +1241,7 @@ function parse($TEXT, options) { break; case "operator": if (!is_identifier_string(tok.value)) { - croak("SyntaxError: Invalid getter/setter name: " + tok.value, + croak("Invalid getter/setter name: " + tok.value, tok.line, tok.col, tok.pos); } ret = _make_symbol(AST_SymbolRef); @@ -1397,7 +1396,7 @@ function parse($TEXT, options) { function as_symbol(type, noerror) { if (!is("name")) { - if (!noerror) croak("SyntaxError: Name expected"); + if (!noerror) croak("Name expected"); return null; } var sym = _make_symbol(type); @@ -1461,7 +1460,7 @@ function parse($TEXT, options) { function make_unary(ctor, op, expr) { if ((op == "++" || op == "--") && !is_assignable(expr)) - croak("SyntaxError: Invalid use of " + op + " operator"); + croak("Invalid use of " + op + " operator"); return new ctor({ operator: op, expression: expr }); }; @@ -1525,7 +1524,7 @@ function parse($TEXT, options) { end : prev() }); } - croak("SyntaxError: Invalid assignment"); + croak("Invalid assignment"); } return left; }; diff --git a/lib/utils.js b/lib/utils.js index a0571d65..46adfd4c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -78,13 +78,28 @@ function repeat_string(str, i) { return d; }; +function configure_error_stack(fn) { + Object.defineProperty(fn.prototype, "stack", { + get: function() { + var err = new Error(this.message); + err.name = this.name; + try { + throw err; + } catch(e) { + return e.stack; + } + } + }); +} + function DefaultsError(msg, defs) { - Error.call(this, msg); - this.msg = msg; + this.message = msg; this.defs = defs; }; DefaultsError.prototype = Object.create(Error.prototype); DefaultsError.prototype.constructor = DefaultsError; +DefaultsError.prototype.name = "DefaultsError"; +configure_error_stack(DefaultsError); DefaultsError.croak = function(msg, defs) { throw new DefaultsError(msg, defs); diff --git a/test/input/invalid/eof.js b/test/input/invalid/eof.js new file mode 100644 index 00000000..330d5023 --- /dev/null +++ b/test/input/invalid/eof.js @@ -0,0 +1 @@ +foo, bar( diff --git a/test/input/invalid/simple.js b/test/input/invalid/simple.js new file mode 100644 index 00000000..98a07d20 --- /dev/null +++ b/test/input/invalid/simple.js @@ -0,0 +1 @@ +function f(a{} diff --git a/test/input/invalid/tab.js b/test/input/invalid/tab.js new file mode 100644 index 00000000..f209b8ee --- /dev/null +++ b/test/input/invalid/tab.js @@ -0,0 +1 @@ + foo( xyz, 0abc); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 52c70935..c07eeee7 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -199,4 +199,43 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should fail with invalid syntax", function(done) { + var command = uglifyjscmd + ' test/input/invalid/simple.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + var lines = stderr.split(/\n/); + assert.strictEqual(lines[0], "Parse error at test/input/invalid/simple.js:1,12"); + assert.strictEqual(lines[1], "function f(a{}"); + assert.strictEqual(lines[2], " ^"); + assert.strictEqual(lines[3], "SyntaxError: Unexpected token punc «{», expected punc «,»"); + done(); + }); + }); + it("Should fail with correct marking of tabs", function(done) { + var command = uglifyjscmd + ' test/input/invalid/tab.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + var lines = stderr.split(/\n/); + assert.strictEqual(lines[0], "Parse error at test/input/invalid/tab.js:1,12"); + assert.strictEqual(lines[1], "\t\tfoo(\txyz, 0abc);"); + assert.strictEqual(lines[2], "\t\t \t ^"); + assert.strictEqual(lines[3], "SyntaxError: Invalid syntax: 0abc"); + done(); + }); + }); + it("Should fail with correct marking at start of line", function(done) { + var command = uglifyjscmd + ' test/input/invalid/eof.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + var lines = stderr.split(/\n/); + assert.strictEqual(lines[0], "Parse error at test/input/invalid/eof.js:2,0"); + assert.strictEqual(lines[1], "foo, bar("); + assert.strictEqual(lines[2], " ^"); + assert.strictEqual(lines[3], "SyntaxError: Unexpected token: eof (undefined)"); + done(); + }); + }); }); diff --git a/test/mocha/comment.js b/test/mocha/comment.js index 69cdb3d5..56470e0f 100644 --- a/test/mocha/comment.js +++ b/test/mocha/comment.js @@ -13,7 +13,7 @@ describe("Comment", function() { var fail = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "SyntaxError: Unexpected token: operator (>)" && + e.message === "Unexpected token: operator (>)" && e.line === 2 && e.col === 0; } @@ -36,7 +36,7 @@ describe("Comment", function() { var fail = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "SyntaxError: Unexpected token: operator (>)" && + e.message === "Unexpected token: operator (>)" && e.line === 5 && e.col === 0; } diff --git a/test/mocha/directives.js b/test/mocha/directives.js index 82594758..bc763ae0 100644 --- a/test/mocha/directives.js +++ b/test/mocha/directives.js @@ -168,7 +168,7 @@ describe("Directives", function() { throw new Error("Expected parser to fail"); } catch (e) { assert.strictEqual(e instanceof uglify.JS_Parse_Error, true); - assert.strictEqual(e.message, "SyntaxError: Unexpected token: punc (])"); + assert.strictEqual(e.message, "Unexpected token: punc (])"); } test_directive(tokenizer, tests[i]); diff --git a/test/mocha/getter-setter.js b/test/mocha/getter-setter.js index a292fa00..641a2026 100644 --- a/test/mocha/getter-setter.js +++ b/test/mocha/getter-setter.js @@ -71,7 +71,7 @@ describe("Getters and setters", function() { var fail = function(data) { return function (e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "SyntaxError: Invalid getter/setter name: " + data.operator; + e.message === "Invalid getter/setter name: " + data.operator; }; }; diff --git a/test/mocha/line-endings.js b/test/mocha/line-endings.js index ef46bccd..10e2a1c5 100644 --- a/test/mocha/line-endings.js +++ b/test/mocha/line-endings.js @@ -50,7 +50,7 @@ describe("line-endings", function() { } var fail = function(e) { return e instanceof Uglify.JS_Parse_Error && - e.message === "SyntaxError: Unexpected line terminator"; + e.message === "Unexpected line terminator"; } for (var i = 0; i < inputs.length; i++) { assert.throws(test(inputs[i]), fail); diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 1b830cb5..0cf8c5c1 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -110,7 +110,7 @@ describe("minify", function() { inSourceMap: "inline", sourceMapInline: true }); - }, "multiple input and inline source map"); + }); }); it("Should fail with SpiderMonkey and inline source map", function() { assert.throws(function() { @@ -119,7 +119,7 @@ describe("minify", function() { sourceMapInline: true, spidermonkey: true }); - }, "SpiderMonkey and inline source map"); + }); }); }); @@ -156,4 +156,19 @@ describe("minify", function() { }); }); + describe("JS_Parse_Error", function() { + it("should throw syntax error", function() { + assert.throws(function() { + Uglify.minify("function f(a{}", { fromString: true }); + }, function(err) { + assert.ok(err instanceof Error); + assert.strictEqual(err.stack.split(/\n/)[0], "SyntaxError: Unexpected token punc «{», expected punc «,»"); + assert.strictEqual(err.filename, 0); + assert.strictEqual(err.line, 1); + assert.strictEqual(err.col, 12); + return true; + }); + }); + }); + }); diff --git a/test/mocha/number-literal.js b/test/mocha/number-literal.js index 8e05574a..e80a5313 100644 --- a/test/mocha/number-literal.js +++ b/test/mocha/number-literal.js @@ -15,7 +15,7 @@ describe("Number literals", function () { } var error = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "SyntaxError: Legacy octal literals are not allowed in strict mode"; + e.message === "Legacy octal literals are not allowed in strict mode"; } for (var i = 0; i < inputs.length; i++) { assert.throws(test(inputs[i]), error, inputs[i]); diff --git a/test/mocha/string-literal.js b/test/mocha/string-literal.js index eb9e6f1c..6e337a24 100644 --- a/test/mocha/string-literal.js +++ b/test/mocha/string-literal.js @@ -19,7 +19,7 @@ describe("String literals", function() { var error = function(e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "SyntaxError: Unterminated string constant"; + e.message === "Unterminated string constant"; }; for (var input in inputs) { @@ -49,7 +49,7 @@ describe("String literals", function() { var error = function(e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "SyntaxError: Legacy octal escape sequences are not allowed in strict mode"; + e.message === "Legacy octal escape sequences are not allowed in strict mode"; } for (var input in inputs) { diff --git a/test/mocha/with.js b/test/mocha/with.js index 734e1e13..a74ef41a 100644 --- a/test/mocha/with.js +++ b/test/mocha/with.js @@ -9,7 +9,7 @@ describe("With", function() { } var error = function(e) { return e instanceof uglify.JS_Parse_Error && - e.message === "SyntaxError: Strict mode may not include a with statement"; + e.message === "Strict mode may not include a with statement"; } assert.throws(test, error); }); From 0b0296eb2aebbb6f3df72d4e4ef22d447fe396ec Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 27 Feb 2017 03:47:54 +0800 Subject: [PATCH 37/37] v2.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2316283f..beec960c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.7.5", + "version": "2.8.0", "engines": { "node": ">=0.8.0" },