From f3a487a36829eabc7b2d878eba299f7d97d830e0 Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 2 Jul 2017 13:37:04 -0400 Subject: [PATCH 01/11] document fast mangle-only minify mode (#2194) --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index c277f653..671d7136 100644 --- a/README.md +++ b/README.md @@ -1026,3 +1026,29 @@ in total it's a bit more than just using UglifyJS's own parser. [acorn]: https://github.com/ternjs/acorn [sm-spec]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k + +### Uglify Fast Minify Mode + +It's not well known, but variable and function name mangling accounts for +95% of the size reduction in minified code for most javascript - not +elaborate code transforms. One can simply disable `compress` to speed up +Uglify builds by 3 to 4 times. In this fast `mangle`-only mode Uglify has +comparable minify speeds and gzip sizes to +[`butternut`](https://www.npmjs.com/package/butternut): + +| d3.js | minify size | gzip size | minify time (seconds) | +| --- | ---: | ---: | ---: | +| original | 451,131 | 108,733 | - | +| uglify-js@3.0.23 mangle=false, compress=false | 316,600 | 85,245 | 0.73 | +| uglify-js@3.0.23 mangle=true, compress=false | 220,216 | 72,730 | 1.21 | +| Butternut 0.4.6 | 217,568 | 72,738 | 1.81 | +| uglify-js@3.0.23 mangle=true, compress=true | 212,511 | 71,560 | 4.64 | + +To enable fast minify mode from the CLI use: +``` +uglifyjs file.js -m +``` +To enable fast minify mode with the API use: +```js +UglifyJS.minify(code, { compress: false, mangle: true }); +``` From 20e4f8277fc69b275600770c5d41fa1b75b9984f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 3 Jul 2017 02:10:56 +0800 Subject: [PATCH 02/11] refactor `throw` usage within `compress` (#2193) Eliminate exceptional constructs from normal control flow. --- lib/compress.js | 207 ++++++++++++++++++++++++++---------------------- 1 file changed, 112 insertions(+), 95 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index eb3ba756..db45c56b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1198,21 +1198,21 @@ merge(Compressor.prototype, { for (var i = 0, len = statements.length; i < len; i++) { var stat = statements[i]; if (prev) { - if (stat instanceof AST_For) { - try { - prev.body.walk(new TreeWalker(function(node){ - if (node instanceof AST_Binary && node.operator == "in") - throw cons_seq; - })); - if (stat.init && !(stat.init instanceof AST_Definitions)) { - stat.init = cons_seq(stat.init); + if (stat instanceof AST_For && !(stat.init instanceof AST_Definitions)) { + var abort = false; + prev.body.walk(new TreeWalker(function(node) { + if (abort) return true; + if (node instanceof AST_Binary && node.operator == "in") { + abort = true; + return true; } - else if (!stat.init) { + })); + if (!abort) { + if (stat.init) stat.init = cons_seq(stat.init); + else { stat.init = prev.body.drop_side_effect_free(compressor); n--; } - } catch(ex) { - if (ex !== cons_seq) throw ex; } } else if (stat instanceof AST_If) { @@ -1532,13 +1532,8 @@ merge(Compressor.prototype, { // descendant of AST_Node. AST_Node.DEFMETHOD("evaluate", function(compressor){ if (!compressor.option("evaluate")) return this; - try { - var val = this._eval(compressor); - return !val || val instanceof RegExp || typeof val != "object" ? val : this; - } catch(ex) { - if (ex !== def) throw ex; - return this; - } + var val = this._eval(compressor); + return !val || val instanceof RegExp || typeof val != "object" ? val : this; }); var unaryPrefix = makePredicate("! ~ - + void"); AST_Node.DEFMETHOD("is_constant", function(){ @@ -1584,27 +1579,28 @@ merge(Compressor.prototype, { def(AST_Statement, function(){ throw new Error(string_template("Cannot evaluate a statement [{file}:{line},{col}]", this.start)); }); - def(AST_Lambda, function(){ - throw def; - }); + def(AST_Lambda, return_this); function ev(node, compressor) { if (!compressor) throw new Error("Compressor must be passed"); return node._eval(compressor); }; - def(AST_Node, function(){ - throw def; // not constant - }); + def(AST_Node, return_this); def(AST_Constant, function(){ return this.getValue(); }); def(AST_Array, function(compressor){ if (compressor.option("unsafe")) { - return this.elements.map(function(element) { - return ev(element, compressor); - }); + var elements = []; + for (var i = 0, len = this.elements.length; i < len; i++) { + var element = this.elements[i]; + var value = ev(element, compressor); + if (element === value) return this; + elements.push(value); + } + return elements; } - throw def; + return this; }); def(AST_Object, function(compressor){ if (compressor.option("unsafe")) { @@ -1616,105 +1612,121 @@ merge(Compressor.prototype, { key = key.name; } else if (key instanceof AST_Node) { key = ev(key, compressor); + if (key === prop.key) return this; } if (typeof Object.prototype[key] === 'function') { - throw def; + return this; } val[key] = ev(prop.value, compressor); + if (val[key] === prop.value) return this; } return val; } - throw def; + return this; }); def(AST_UnaryPrefix, function(compressor){ - var e = this.expression; + // Function would be evaluated to an array and so typeof would + // incorrectly return 'object'. Hence making is a special case. + if (this.operator == "typeof" && this.expression instanceof AST_Function) { + return typeof function(){}; + } + var e = ev(this.expression, compressor); + if (e === this.expression) return this; switch (this.operator) { - case "!": return !ev(e, compressor); + case "!": return !e; case "typeof": - // Function would be evaluated to an array and so typeof would - // incorrectly return 'object'. Hence making is a special case. - if (e instanceof AST_Function) return typeof function(){}; - - e = ev(e, compressor); - // typeof returns "object" or "function" on different platforms // so cannot evaluate reliably - if (e instanceof RegExp) throw def; - + if (e instanceof RegExp) return this; return typeof e; - case "void": return void ev(e, compressor); - case "~": return ~ev(e, compressor); - case "-": return -ev(e, compressor); - case "+": return +ev(e, compressor); + case "void": return void e; + case "~": return ~e; + case "-": return -e; + case "+": return +e; } - throw def; + return this; }); - def(AST_Binary, function(c){ - var left = this.left, right = this.right, result; + def(AST_Binary, function(compressor){ + var left = ev(this.left, compressor); + if (left === this.left) return this; + var right = ev(this.right, compressor); + if (right === this.right) return this; + var result; switch (this.operator) { - case "&&" : result = ev(left, c) && ev(right, c); break; - case "||" : result = ev(left, c) || ev(right, c); break; - case "|" : result = ev(left, c) | ev(right, c); break; - case "&" : result = ev(left, c) & ev(right, c); break; - case "^" : result = ev(left, c) ^ ev(right, c); break; - case "+" : result = ev(left, c) + ev(right, c); break; - case "*" : result = ev(left, c) * ev(right, c); break; - case "/" : result = ev(left, c) / ev(right, c); break; - case "%" : result = ev(left, c) % ev(right, c); break; - case "-" : result = ev(left, c) - ev(right, c); break; - case "<<" : result = ev(left, c) << ev(right, c); break; - case ">>" : result = ev(left, c) >> ev(right, c); break; - case ">>>" : result = ev(left, c) >>> ev(right, c); break; - case "==" : result = ev(left, c) == ev(right, c); break; - case "===" : result = ev(left, c) === ev(right, c); break; - case "!=" : result = ev(left, c) != ev(right, c); break; - case "!==" : result = ev(left, c) !== ev(right, c); break; - case "<" : result = ev(left, c) < ev(right, c); break; - case "<=" : result = ev(left, c) <= ev(right, c); break; - case ">" : result = ev(left, c) > ev(right, c); break; - case ">=" : result = ev(left, c) >= ev(right, c); break; + case "&&" : result = left && right; break; + case "||" : result = left || right; break; + case "|" : result = left | right; break; + case "&" : result = left & right; break; + case "^" : result = left ^ right; break; + case "+" : result = left + right; break; + case "*" : result = left * right; break; + case "/" : result = left / right; break; + case "%" : result = left % right; break; + case "-" : result = left - right; break; + case "<<" : result = left << right; break; + case ">>" : result = left >> right; break; + case ">>>" : result = left >>> right; break; + case "==" : result = left == right; break; + case "===" : result = left === right; break; + case "!=" : result = left != right; break; + case "!==" : result = left !== right; break; + case "<" : result = left < right; break; + case "<=" : result = left <= right; break; + case ">" : result = left > right; break; + case ">=" : result = left >= right; break; default: - throw def; + return this; } - if (isNaN(result) && c.find_parent(AST_With)) { + if (isNaN(result) && compressor.find_parent(AST_With)) { // leave original expression as is - throw def; + return this; } return result; }); def(AST_Conditional, function(compressor){ - return ev(this.condition, compressor) - ? ev(this.consequent, compressor) - : ev(this.alternative, compressor); + var condition = ev(this.condition, compressor); + if (condition === this.condition) return this; + var node = condition ? this.consequent : this.alternative; + var value = ev(node, compressor); + return value === node ? this : value; }); def(AST_SymbolRef, function(compressor){ - if (!compressor.option("reduce_vars") || this._evaluating) throw def; - this._evaluating = true; - try { - var fixed = this.fixed_value(); - if (!fixed) throw def; - var value = ev(fixed, compressor); - if (!HOP(fixed, "_eval")) fixed._eval = function() { - return value; - }; - if (value && typeof value == "object" && this.definition().escaped) throw def; - return value; - } finally { - this._evaluating = false; + if (!compressor.option("reduce_vars")) return this; + this._eval = return_this; + var fixed = this.fixed_value(); + if (!fixed) { + delete this._eval; + return this; } + var value = ev(fixed, compressor); + if (value === fixed) { + delete this._eval; + return this; + } + if (!HOP(fixed, "_eval")) fixed._eval = function() { + return value; + }; + if (value && typeof value == "object" && this.definition().escaped) { + delete this._eval; + return this; + } + this._eval = fixed._eval; + return value; }); def(AST_PropAccess, function(compressor){ if (compressor.option("unsafe")) { var key = this.property; if (key instanceof AST_Node) { key = ev(key, compressor); + if (key === this.property) return this; } var val = ev(this.expression, compressor); + if (val === this.expression) return this; if (val && HOP(val, key)) { return val[key]; } } - throw def; + return this; }); var object_fns = [ 'constructor', @@ -1760,19 +1772,24 @@ merge(Compressor.prototype, { var key = exp.property; if (key instanceof AST_Node) { key = ev(key, compressor); + if (key === exp.property) return this; } var val = ev(exp.expression, compressor); + if (val === exp.expression) return this; if ((val && native_fns[val.constructor.name] || return_false)(key)) { - return val[key].apply(val, this.args.map(function(arg) { - return ev(arg, compressor); - })); + var args = []; + for (var i = 0, len = this.args.length; i < len; i++) { + var arg = this.args[i]; + var value = ev(arg, compressor); + if (arg === value) return this; + args.push(value); + } + return val[key].apply(val, args); } } - throw def; - }); - def(AST_New, function(compressor){ - throw def; + return this; }); + def(AST_New, return_this); })(function(node, func){ node.DEFMETHOD("_eval", func); }); From 6b3aeff1d8975f769f7e251c23405baf3a6f2b5a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 3 Jul 2017 03:23:38 +0800 Subject: [PATCH 03/11] clean up `TreeWalker.pop()` (#2195) Remove superfluous parameter. --- lib/ast.js | 7 +++---- lib/transform.js | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 2972b7aa..0918574d 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -859,7 +859,7 @@ TreeWalker.prototype = { if (!ret && descend) { descend.call(node); } - this.pop(node); + this.pop(); return ret; }, parent: function(n) { @@ -873,9 +873,8 @@ TreeWalker.prototype = { } this.stack.push(node); }, - pop: function(node) { - this.stack.pop(); - if (node instanceof AST_Lambda) { + pop: function() { + if (this.stack.pop() instanceof AST_Lambda) { this.directives = Object.getPrototypeOf(this.directives); } }, diff --git a/lib/transform.js b/lib/transform.js index 112e5f28..8008e571 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -70,7 +70,7 @@ TreeTransformer.prototype = new TreeWalker; if (y !== undefined) x = y; } } - tw.pop(this); + tw.pop(); return x; }); }; From af0262b7e5fd3dbf83619cdb375ab18c41def3e7 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 3 Jul 2017 04:17:37 +0800 Subject: [PATCH 04/11] improve parenthesis emission (#2196) - eliminate `throw` usages - suppress extraneous parenthesis - `new function() {foo.bar()}.baz` - `for (function() { "foo" in bar; };;);` --- lib/compress.js | 2 +- lib/output.js | 41 +++++++++++++++++++------------------- test/compress/loops.js | 14 +++++++++++++ test/compress/new.js | 16 +++++++++++++++ test/compress/sequences.js | 9 +++++++++ 5 files changed, 60 insertions(+), 22 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index db45c56b..f0aedf57 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1201,7 +1201,7 @@ merge(Compressor.prototype, { if (stat instanceof AST_For && !(stat.init instanceof AST_Definitions)) { var abort = false; prev.body.walk(new TreeWalker(function(node) { - if (abort) return true; + if (abort || node instanceof AST_Scope) return true; if (node instanceof AST_Binary && node.operator == "in") { abort = true; return true; diff --git a/lib/output.js b/lib/output.js index 92c72484..9416583a 100644 --- a/lib/output.js +++ b/lib/output.js @@ -672,14 +672,15 @@ function OutputStream(options) { // parens around it too, otherwise the call will be // interpreted as passing the arguments to the upper New // expression. - try { - this.walk(new TreeWalker(function(node){ - if (node instanceof AST_Call) throw p; - })); - } catch(ex) { - if (ex !== p) throw ex; - return true; - } + var parens = false; + this.walk(new TreeWalker(function(node) { + if (parens || node instanceof AST_Scope) return true; + if (node instanceof AST_Call) { + parens = true; + return true; + } + })); + return parens; } }); @@ -1073,19 +1074,17 @@ function OutputStream(options) { }); function parenthesize_for_noin(node, output, noin) { - if (!noin) node.print(output); - else try { - // need to take some precautions here: - // https://github.com/mishoo/UglifyJS2/issues/60 - node.walk(new TreeWalker(function(node){ - if (node instanceof AST_Binary && node.operator == "in") - throw output; - })); - node.print(output); - } catch(ex) { - if (ex !== output) throw ex; - node.print(output, true); - } + var parens = false; + // need to take some precautions here: + // https://github.com/mishoo/UglifyJS2/issues/60 + if (noin) node.walk(new TreeWalker(function(node) { + if (parens || node instanceof AST_Scope) return true; + if (node instanceof AST_Binary && node.operator == "in") { + parens = true; + return true; + } + })); + node.print(output, parens); }; DEFPRINT(AST_VarDef, function(self, output){ diff --git a/test/compress/loops.js b/test/compress/loops.js index 89c7e7e9..bac40494 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -436,3 +436,17 @@ do_switch: { } while (false); } } + +in_parenthesis_1: { + input: { + for (("foo" in {});0;); + } + expect_exact: 'for(("foo"in{});0;);' +} + +in_parenthesis_2: { + input: { + for ((function(){ "foo" in {}; });0;); + } + expect_exact: 'for(function(){"foo"in{}};0;);' +} diff --git a/test/compress/new.js b/test/compress/new.js index 83da88e6..640f201a 100644 --- a/test/compress/new.js +++ b/test/compress/new.js @@ -82,3 +82,19 @@ new_with_unary_prefix: { } expect_exact: 'var bar=(+new Date).toString(32);'; } + +dot_parenthesis_1: { + input: { + console.log(new (Math.random().constructor) instanceof Number); + } + expect_exact: "console.log(new(Math.random().constructor)instanceof Number);" + expect_stdout: "true" +} + +dot_parenthesis_2: { + input: { + console.log(typeof new function(){Math.random()}.constructor); + } + expect_exact: "console.log(typeof new function(){Math.random()}.constructor);" + expect_stdout: "function" +} diff --git a/test/compress/sequences.js b/test/compress/sequences.js index f41b603f..5ce24ac0 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -176,6 +176,11 @@ for_sequences: { // 4 x = (foo in bar); for (y = 5; false;); + // 5 + x = function() { + foo in bar; + }; + for (y = 5; false;); } expect: { // 1 @@ -188,6 +193,10 @@ for_sequences: { // 4 x = (foo in bar); for (y = 5; false;); + // 5 + for (x = function() { + foo in bar; + }, y = 5; false;); } } From 5f046c724bf0910e6ee7c1c3feabaf62625fa1c6 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 3 Jul 2017 18:52:39 +0800 Subject: [PATCH 05/11] minor clean-ups to `evaluate` (#2197) --- lib/compress.js | 57 +++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f0aedf57..733abf45 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1692,12 +1692,9 @@ merge(Compressor.prototype, { }); def(AST_SymbolRef, function(compressor){ if (!compressor.option("reduce_vars")) return this; - this._eval = return_this; var fixed = this.fixed_value(); - if (!fixed) { - delete this._eval; - return this; - } + if (!fixed) return this; + this._eval = return_this; var value = ev(fixed, compressor); if (value === fixed) { delete this._eval; @@ -1729,41 +1726,41 @@ merge(Compressor.prototype, { return this; }); var object_fns = [ - 'constructor', - 'toString', - 'valueOf', + "constructor", + "toString", + "valueOf", ]; var native_fns = { Array: makePredicate([ - 'indexOf', - 'join', - 'lastIndexOf', - 'slice', + "indexOf", + "join", + "lastIndexOf", + "slice", ].concat(object_fns)), Boolean: makePredicate(object_fns), Number: makePredicate([ - 'toExponential', - 'toFixed', - 'toPrecision', + "toExponential", + "toFixed", + "toPrecision", ].concat(object_fns)), RegExp: makePredicate([ - 'test', + "test", ].concat(object_fns)), String: makePredicate([ - 'charAt', - 'charCodeAt', - 'concat', - 'indexOf', - 'italics', - 'lastIndexOf', - 'match', - 'replace', - 'search', - 'slice', - 'split', - 'substr', - 'substring', - 'trim', + "charAt", + "charCodeAt", + "concat", + "indexOf", + "italics", + "lastIndexOf", + "match", + "replace", + "search", + "slice", + "split", + "substr", + "substring", + "trim", ].concat(object_fns)), }; def(AST_Call, function(compressor){ From 1ac25fc032096459f2966236c99c0f21da2787f3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 5 Jul 2017 19:20:33 +0800 Subject: [PATCH 06/11] improve `compress` granularity through `typeofs` (#2201) fixes #2198 --- README.md | 7 +++++-- lib/compress.js | 4 +++- test/compress/issue-1446.js | 10 +++++++--- test/compress/reduce_vars.js | 2 ++ 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 671d7136..fd142f4b 100644 --- a/README.md +++ b/README.md @@ -644,6 +644,10 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `booleans` -- various optimizations for boolean context, for example `!!a ? b : c → a ? b : c` +- `typeofs` -- default `true`. Transforms `typeof foo == "undefined"` into + `foo === void 0`. Note: recommend to set this value to `false` for IE10 and + earlier versions due to known issues. + - `loops` -- optimizations for `do`, `while` and `for` loops when we can statically determine the condition @@ -873,7 +877,6 @@ when this flag is on: - `new Object()` → `{}` - `String(exp)` or `exp.toString()` → `"" + exp` - `new Object/RegExp/Function/Error/Array (...)` → we discard the `new` -- `typeof foo == "undefined"` → `foo === void 0` - `void 0` → `undefined` (if there is a variable named "undefined" in scope; we do it because the variable name will be mangled, typically reduced to a single character) @@ -1050,5 +1053,5 @@ uglifyjs file.js -m ``` To enable fast minify mode with the API use: ```js -UglifyJS.minify(code, { compress: false, mangle: true }); +UglifyJS.minify(code, { compress: false, mangle: true }); ``` diff --git a/lib/compress.js b/lib/compress.js index 733abf45..d9936d2c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -80,6 +80,7 @@ function Compressor(options, false_by_default) { switches : !false_by_default, top_retain : null, toplevel : !!(options && options["top_retain"]), + typeofs : !false_by_default, unsafe : false, unsafe_comps : false, unsafe_Func : false, @@ -3591,7 +3592,8 @@ merge(Compressor.prototype, { case "==": case "!=": // "undefined" == typeof x => undefined === x - if (self.left instanceof AST_String + if (compressor.option("typeofs") + && self.left instanceof AST_String && self.left.value == "undefined" && self.right instanceof AST_UnaryPrefix && self.right.operator == "typeof") { diff --git a/test/compress/issue-1446.js b/test/compress/issue-1446.js index cad1ae57..30062c7d 100644 --- a/test/compress/issue-1446.js +++ b/test/compress/issue-1446.js @@ -1,6 +1,7 @@ typeof_eq_undefined: { options = { - comparisons: true + comparisons: true, + typeofs: true, } input: { var a = typeof b != "undefined"; @@ -24,6 +25,7 @@ typeof_eq_undefined_ie8: { options = { comparisons: true, ie8: true, + typeofs: true, } input: { var a = typeof b != "undefined"; @@ -45,7 +47,8 @@ typeof_eq_undefined_ie8: { undefined_redefined: { options = { - comparisons: true + comparisons: true, + typeofs: true, } input: { function f(undefined) { @@ -58,7 +61,8 @@ undefined_redefined: { undefined_redefined_mangle: { options = { - comparisons: true + comparisons: true, + typeofs: true, } mangle = {} input: { diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 5906a971..556bcad2 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1469,6 +1469,7 @@ issue_1670_1: { reduce_vars: true, side_effects: true, switches: true, + typeofs: true, unused: true, } input: { @@ -1532,6 +1533,7 @@ issue_1670_3: { reduce_vars: true, side_effects: true, switches: true, + typeofs: true, unused: true, } input: { From 9306da3c58831fabc93dfae6a7ea4f45d42183d4 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 6 Jul 2017 01:03:52 +0800 Subject: [PATCH 07/11] suppress `collapse_vars` of `this` as call argument (#2204) fixes #2203 --- lib/compress.js | 25 ++++++++----- test/compress/collapse_vars.js | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index d9936d2c..a2acd53f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -823,16 +823,23 @@ merge(Compressor.prototype, { fn.argnames.forEach(function(sym, i) { var arg = iife.args[i]; if (!arg) arg = make_node(AST_Undefined, sym); - else arg.walk(new TreeWalker(function(node) { - if (!arg) return true; - if (node instanceof AST_SymbolRef && fn.variables.has(node.name)) { - var s = node.definition().scope; - if (s !== scope) while (s = s.parent_scope) { - if (s === scope) return true; + else { + var tw = new TreeWalker(function(node) { + if (!arg) return true; + if (node instanceof AST_SymbolRef && fn.variables.has(node.name)) { + var s = node.definition().scope; + if (s !== scope) while (s = s.parent_scope) { + if (s === scope) return true; + } + arg = null; } - arg = null; - } - })); + if (node instanceof AST_This && !tw.find_parent(AST_Scope)) { + arg = null; + return true; + } + }); + arg.walk(tw); + } if (arg) candidates.push(make_node(AST_VarDef, sym, { name: sym, value: arg diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 24f8ffa3..7f3c470b 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -2256,3 +2256,67 @@ issue_2187_3: { } expect_stdout: "1" } + +issue_2203_1: { + options = { + collapse_vars: true, + unused: true, + } + input: { + a = "FAIL"; + console.log({ + a: "PASS", + b: function() { + return function(c) { + return c.a; + }((String, (Object, this))); + } + }.b()); + } + expect: { + a = "FAIL"; + console.log({ + a: "PASS", + b: function() { + return function(c) { + return c.a; + }((String, (Object, this))); + } + }.b()); + } + expect_stdout: "PASS" +} + +issue_2203_2: { + options = { + collapse_vars: true, + unused: true, + } + input: { + a = "PASS"; + console.log({ + a: "FAIL", + b: function() { + return function(c) { + return c.a; + }((String, (Object, function() { + return this; + }()))); + } + }.b()); + } + expect: { + a = "PASS"; + console.log({ + a: "FAIL", + b: function() { + return function(c) { + return (String, (Object, function() { + return this; + }())).a; + }(); + } + }.b()); + } + expect_stdout: "PASS" +} From 4b6ca5e742787c59969b9b00442cf85bbec19ed5 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 6 Jul 2017 21:51:58 +0800 Subject: [PATCH 08/11] inline property access of object literal (#2209) - only if property value is side-effect-free - guard by `unsafe` fixes #2208 --- lib/compress.js | 25 ++++++++ test/compress/evaluate.js | 92 ++++++++++++++++------------ test/compress/global_defs.js | 5 +- test/compress/properties.js | 113 +++++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 40 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index a2acd53f..76eb6918 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4307,6 +4307,17 @@ merge(Compressor.prototype, { return self; }); + AST_Lambda.DEFMETHOD("contains_this", function() { + var result; + var self = this; + self.walk(new TreeWalker(function(node) { + if (result) return true; + if (node instanceof AST_This) return result = true; + if (node !== self && node instanceof AST_Scope) return true; + })); + return result; + }); + OPT(AST_Dot, function(self, compressor){ var def = self.resolve_defines(compressor); if (def) { @@ -4321,6 +4332,20 @@ merge(Compressor.prototype, { }) }).optimize(compressor); } + if (compressor.option("unsafe") && self.expression instanceof AST_Object) { + var values = self.expression.properties; + for (var i = values.length; --i >= 0;) { + if (values[i].key === prop) { + var value = values[i].value; + if (value instanceof AST_Function ? !value.contains_this() : !value.has_side_effects(compressor)) { + var obj = self.expression.clone(); + obj.properties = obj.properties.slice(); + obj.properties.splice(i, 1); + return make_sequence(self, [ obj, value ]).optimize(compressor); + } + } + } + } if (compressor.option("unsafe_proto") && self.expression instanceof AST_Dot && self.expression.property == "prototype") { diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 27d08d47..69ea8c19 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -250,22 +250,26 @@ unsafe_constant: { unsafe_object: { options = { - evaluate : true, - unsafe : true + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, } input: { + var o = { a: 1 }; console.log( - ({a:1}) + 1, - ({a:1}).a + 1, - ({a:1}).b + 1, - ({a:1}).a.b + 1 + o + 1, + o.a + 1, + o.b + 1, + o.a.b + 1 ); } expect: { + var o = { a: 1 }; console.log( - ({a:1}) + 1, + o + 1, 2, - ({a:1}).b + 1, + o.b + 1, 1..b + 1 ); } @@ -274,22 +278,26 @@ unsafe_object: { unsafe_object_nested: { options = { - evaluate : true, - unsafe : true + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, } input: { + var o = { a: { b: 1 } }; console.log( - ({a:{b:1}}) + 1, - ({a:{b:1}}).a + 1, - ({a:{b:1}}).b + 1, - ({a:{b:1}}).a.b + 1 + o + 1, + o.a + 1, + o.b + 1, + o.a.b + 1 ); } expect: { + var o = { a: { b: 1 } }; console.log( - ({a:{b:1}}) + 1, - ({a:{b:1}}).a + 1, - ({a:{b:1}}).b + 1, + o + 1, + o.a + 1, + o.b + 1, 2 ); } @@ -298,21 +306,25 @@ unsafe_object_nested: { unsafe_object_complex: { options = { - evaluate : true, - unsafe : true + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, } input: { + var o = { a: { b: 1 }, b: 1 }; console.log( - ({a:{b:1},b:1}) + 1, - ({a:{b:1},b:1}).a + 1, - ({a:{b:1},b:1}).b + 1, - ({a:{b:1},b:1}).a.b + 1 + o + 1, + o.a + 1, + o.b + 1, + o.a.b + 1 ); } expect: { + var o = { a: { b: 1 }, b: 1 }; console.log( - ({a:{b:1},b:1}) + 1, - ({a:{b:1},b:1}).a + 1, + o + 1, + o.a + 1, 2, 2 ); @@ -322,22 +334,26 @@ unsafe_object_complex: { unsafe_object_repeated: { options = { - evaluate : true, - unsafe : true + evaluate: true, + reduce_vars: true, + toplevel: true, + unsafe: true, } input: { + var o = { a: { b: 1 }, a: 1 }; console.log( - ({a:{b:1},a:1}) + 1, - ({a:{b:1},a:1}).a + 1, - ({a:{b:1},a:1}).b + 1, - ({a:{b:1},a:1}).a.b + 1 + o + 1, + o.a + 1, + o.b + 1, + o.a.b + 1 ); } expect: { + var o = { a: { b: 1 }, a: 1 }; console.log( - ({a:{b:1},a:1}) + 1, + o + 1, 2, - ({a:{b:1},a:1}).b + 1, + o.b + 1, 1..b + 1 ); } @@ -386,9 +402,9 @@ unsafe_function: { expect: { console.log( ({a:{b:1},b:function(){}}) + 1, - ({a:{b:1},b:function(){}}).a + 1, - ({a:{b:1},b:function(){}}).b + 1, - ({a:{b:1},b:function(){}}).a.b + 1 + ({b:function(){}}, {b:1}) + 1, + ({a:{b:1}}, function(){}) + 1, + ({b:function(){}}, {b:1}).b + 1 ); } expect_stdout: true @@ -636,8 +652,8 @@ unsafe_prototype_function: { var d = ({toString: 0}) + ""; var e = (({valueOf: 0}) + "")[2]; var f = (({toString: 0}) + "")[2]; - var g = ({valueOf: 0}).valueOf(); - var h = "" + ({toString: 0}); + var g = ({}, 0)(); + var h = ({}, 0)(); } } diff --git a/test/compress/global_defs.js b/test/compress/global_defs.js index 74147ded..bd791e2d 100644 --- a/test/compress/global_defs.js +++ b/test/compress/global_defs.js @@ -37,6 +37,7 @@ object: { VALUE: 42, }, }, + side_effects: true, unsafe: true, } input: { @@ -140,9 +141,9 @@ mixed: { 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]', + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:128,22]', + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:130,8]', ] } diff --git a/test/compress/properties.js b/test/compress/properties.js index 8126d6c6..a5527de3 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -657,3 +657,116 @@ accessor_this: { expect_exact: 'var a=1;var b={get this(){return a},set this(c){a=c}};console.log(b.this,b.this=2,b.this);' expect_stdout: "1 2 2" } + +issue_2208_1: { + options = { + inline: true, + side_effects: true, + unsafe: true, + } + input: { + console.log({ + p: function() { + return 42; + } + }.p()); + } + expect: { + console.log(42); + } + expect_stdout: "42" +} + +issue_2208_2: { + options = { + inline: true, + side_effects: true, + unsafe: true, + } + input: { + console.log({ + a: 42, + p: function() { + return this.a; + } + }.p()); + } + expect: { + console.log({ + a: 42, + p: function() { + return this.a; + } + }.p()); + } + expect_stdout: "42" +} + +issue_2208_3: { + options = { + inline: true, + side_effects: true, + unsafe: true, + } + input: { + a = 42; + console.log({ + p: function() { + return function() { + return this.a; + }(); + } + }.p()); + } + expect: { + a = 42; + console.log(function() { + return this.a; + }()); + } + expect_stdout: "42" +} + +issue_2208_4: { + options = { + inline: true, + side_effects: true, + unsafe: true, + } + input: { + function foo() {} + console.log({ + a: foo(), + p: function() { + return 42; + } + }.p()); + } + expect: { + function foo() {} + console.log((foo(), function() { + return 42; + })()); + } + expect_stdout: "42" +} + +issue_2208_5: { + options = { + inline: true, + side_effects: true, + unsafe: true, + } + input: { + console.log({ + p: "FAIL", + p: function() { + return 42; + } + }.p()); + } + expect: { + console.log(42); + } + expect_stdout: "42" +} From 4f70d2e28c9ffd6404756ed3ebf08a448aef5257 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 7 Jul 2017 05:35:32 +0800 Subject: [PATCH 09/11] inlining of static methods & constants (#2211) - guard by `unsafe` - support `Array`, `Math`, `Number`, `Object` & `String` fixes #2207 --- lib/compress.js | 129 +++++++++++++++++++++++++++++++------- test/compress/evaluate.js | 72 +++++++++++++++++++++ 2 files changed, 177 insertions(+), 24 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 76eb6918..e6349242 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1718,6 +1718,40 @@ merge(Compressor.prototype, { this._eval = fixed._eval; return value; }); + var global_objs = { + Array: Array, + Boolean: Boolean, + Math: Math, + Number: Number, + RegExp: RegExp, + Object: Object, + String: String, + }; + function convert_to_predicate(obj) { + for (var key in obj) { + obj[key] = makePredicate(obj[key]); + } + } + var static_values = { + Math: [ + "E", + "LN10", + "LN2", + "LOG2E", + "LOG10E", + "PI", + "SQRT1_2", + "SQRT2", + ], + Number: [ + "MAX_VALUE", + "MIN_VALUE", + "NaN", + "NEGATIVE_INFINITY", + "POSITIVE_INFINITY", + ], + }; + convert_to_predicate(static_values); def(AST_PropAccess, function(compressor){ if (compressor.option("unsafe")) { var key = this.property; @@ -1725,11 +1759,16 @@ merge(Compressor.prototype, { key = ev(key, compressor); if (key === this.property) return this; } - var val = ev(this.expression, compressor); - if (val === this.expression) return this; - if (val && HOP(val, key)) { - return val[key]; + var exp = this.expression; + var val; + if (exp instanceof AST_SymbolRef && exp.undeclared()) { + if (!(static_values[exp.name] || return_false)(key)) return this; + val = global_objs[exp.name]; + } else { + val = ev(exp, compressor); + if (!val || val === exp || !HOP(val, key)) return this; } + return val[key]; } return this; }); @@ -1739,22 +1778,22 @@ merge(Compressor.prototype, { "valueOf", ]; var native_fns = { - Array: makePredicate([ + Array: [ "indexOf", "join", "lastIndexOf", "slice", - ].concat(object_fns)), - Boolean: makePredicate(object_fns), - Number: makePredicate([ + ].concat(object_fns), + Boolean: object_fns, + Number: [ "toExponential", "toFixed", "toPrecision", - ].concat(object_fns)), - RegExp: makePredicate([ + ].concat(object_fns), + RegExp: [ "test", - ].concat(object_fns)), - String: makePredicate([ + ].concat(object_fns), + String: [ "charAt", "charCodeAt", "concat", @@ -1769,8 +1808,45 @@ merge(Compressor.prototype, { "substr", "substring", "trim", - ].concat(object_fns)), + ].concat(object_fns), }; + convert_to_predicate(native_fns); + var static_fns = { + Array: [ + "isArray", + ], + Math: [ + "abs", + "acos", + "asin", + "atan", + "ceil", + "cos", + "exp", + "floor", + "log", + "round", + "sin", + "sqrt", + "tan", + "atan2", + "pow", + "max", + "min" + ], + Number: [ + "isFinite", + "isNaN", + ], + Object: [ + "keys", + "getOwnPropertyNames", + ], + String: [ + "fromCharCode", + ], + }; + convert_to_predicate(static_fns); def(AST_Call, function(compressor){ var exp = this.expression; if (compressor.option("unsafe") && exp instanceof AST_PropAccess) { @@ -1779,18 +1855,23 @@ merge(Compressor.prototype, { key = ev(key, compressor); if (key === exp.property) return this; } - var val = ev(exp.expression, compressor); - if (val === exp.expression) return this; - if ((val && native_fns[val.constructor.name] || return_false)(key)) { - var args = []; - for (var i = 0, len = this.args.length; i < len; i++) { - var arg = this.args[i]; - var value = ev(arg, compressor); - if (arg === value) return this; - args.push(value); - } - return val[key].apply(val, args); + var val; + var e = exp.expression; + if (e instanceof AST_SymbolRef && e.undeclared()) { + if (!(static_fns[e.name] || return_false)(key)) return this; + val = global_objs[e.name]; + } else { + val = ev(e, compressor); + if (val === e || !(val && native_fns[val.constructor.name] || return_false)(key)) return this; } + var args = []; + for (var i = 0, len = this.args.length; i < len; i++) { + var arg = this.args[i]; + var value = ev(arg, compressor); + if (arg === value) return this; + args.push(value); + } + return val[key].apply(val, args); } return this; }); diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 69ea8c19..38e9cdcb 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -1085,3 +1085,75 @@ string_charCodeAt: { } expect_stdout: "NaN" } + +issue_2207_1: { + options = { + evaluate: true, + unsafe: true, + } + input: { + console.log(String.fromCharCode(65)); + console.log(Math.max(3, 6, 2, 7, 3, 4)); + console.log(Math.cos(1.2345)); + console.log(Math.cos(1.2345) - Math.sin(4.321)); + console.log(Math.pow(Math.PI, Math.E - Math.LN10)); + } + expect: { + console.log("A"); + console.log(7); + console.log(Math.cos(1.2345)); + console.log(1.2543732512566947); + console.log(1.6093984514472044); + } + expect_stdout: true +} + +issue_2207_2: { + options = { + evaluate: true, + unsafe: true, + } + input: { + console.log(Math.E); + console.log(Math.LN10); + console.log(Math.LN2); + console.log(Math.LOG2E); + console.log(Math.LOG10E); + console.log(Math.PI); + console.log(Math.SQRT1_2); + console.log(Math.SQRT2); + } + expect: { + console.log(Math.E); + console.log(Math.LN10); + console.log(Math.LN2); + console.log(Math.LOG2E); + console.log(Math.LOG10E); + console.log(Math.PI); + console.log(Math.SQRT1_2); + console.log(Math.SQRT2); + } + expect_stdout: true +} + +issue_2207_3: { + options = { + evaluate: true, + unsafe: true, + } + input: { + console.log(Number.MAX_VALUE); + console.log(Number.MIN_VALUE); + console.log(Number.NaN); + console.log(Number.NEGATIVE_INFINITY); + console.log(Number.POSITIVE_INFINITY); + } + expect: { + console.log(Number.MAX_VALUE); + console.log(5e-324); + console.log(NaN); + console.log(-1/0); + console.log(1/0); + } + expect_stdout: true +} From 71ee91e716a7fb0f1ef8a4a80a627e10944ef062 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 8 Jul 2017 04:42:35 +0800 Subject: [PATCH 10/11] handle duplicate argument names in `collapse_vars` (#2215) --- lib/compress.js | 10 +++++++--- test/compress/collapse_vars.js | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index e6349242..74fb62ec 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -820,7 +820,11 @@ merge(Compressor.prototype, { && !fn.uses_eval && (iife = compressor.parent()) instanceof AST_Call && iife.expression === fn) { - fn.argnames.forEach(function(sym, i) { + var names = Object.create(null); + for (var i = fn.argnames.length; --i >= 0;) { + var sym = fn.argnames[i]; + if (sym.name in names) continue; + names[sym.name] = true; var arg = iife.args[i]; if (!arg) arg = make_node(AST_Undefined, sym); else { @@ -840,11 +844,11 @@ merge(Compressor.prototype, { }); arg.walk(tw); } - if (arg) candidates.push(make_node(AST_VarDef, sym, { + if (arg) candidates.unshift(make_node(AST_VarDef, sym, { name: sym, value: arg })); - }); + } } } diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 7f3c470b..10c403fa 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -2320,3 +2320,25 @@ issue_2203_2: { } expect_stdout: "PASS" } + +duplicate_argname: { + options = { + collapse_vars: true, + unused: true, + } + input: { + function f() { return "PASS"; } + console.log(function(a, a) { + f++; + return a; + }("FAIL", f())); + } + expect: { + function f() { return "PASS"; } + console.log(function(a, a) { + f++; + return a; + }("FAIL", f())); + } + expect_stdout: "PASS" +} From bd7be07c38c3a6e47910828279c87a1e5531c357 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 8 Jul 2017 12:53:20 +0800 Subject: [PATCH 11/11] v3.0.24 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab311195..1185c8cc 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": "3.0.23", + "version": "3.0.24", "engines": { "node": ">=0.8.0" },