diff --git a/README.md b/README.md index 9e31d0f6..01ae1e19 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,8 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `pure_getters` -- the default is `false`. If you pass `true` for this, UglifyJS will assume that object property access (e.g. `foo.bar` or `foo["bar"]`) doesn't have any side effects. + Specify `"strict"` to treat `foo.bar` as side-effect-free only when + `foo` is certain to not throw, i.e. not `null` or `undefined`. - `pure_funcs` -- default `null`. You can pass an array of names and UglifyJS will assume that those functions do not produce side diff --git a/lib/ast.js b/lib/ast.js index 5991bc8b..c5f86d9f 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1379,16 +1379,16 @@ TreeWalker.prototype = { self = p; } }, - loopcontrol_target: function(label) { + loopcontrol_target: function(node) { var stack = this.stack; - if (label) for (var i = stack.length; --i >= 0;) { + if (node.label) for (var i = stack.length; --i >= 0;) { var x = stack[i]; - if (x instanceof AST_LabeledStatement && x.label.name == label.name) { + if (x instanceof AST_LabeledStatement && x.label.name == node.label.name) return x.body; - } } else for (var i = stack.length; --i >= 0;) { var x = stack[i]; - if (x instanceof AST_Switch || x instanceof AST_IterationStatement) + if (x instanceof AST_IterationStatement + || node instanceof AST_Break && x instanceof AST_Switch) return x; } } diff --git a/lib/compress.js b/lib/compress.js index c63a64c6..21b29e29 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -72,7 +72,7 @@ function Compressor(options, false_by_default) { negate_iife : !false_by_default, passes : 1, properties : !false_by_default, - pure_getters : false, + pure_getters : !false_by_default && "strict", pure_funcs : null, reduce_vars : !false_by_default, screw_ie8 : true, @@ -252,9 +252,7 @@ merge(Compressor.prototype, { AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan){ var reduce_vars = rescan && compressor.option("reduce_vars"); var toplevel = compressor.option("toplevel"); - var ie8 = !compressor.option("screw_ie8"); - var safe_ids = []; - push(); + var safe_ids = Object.create(null); var suppressor = new TreeWalker(function(node) { if (node instanceof AST_Symbol) { var d = node.definition(); @@ -263,10 +261,8 @@ merge(Compressor.prototype, { } }); var tw = new TreeWalker(function(node, descend){ - if (!(node instanceof AST_Directive || node instanceof AST_Constant)) { - node._squeezed = false; - node._optimized = false; - } + node._squeezed = false; + node._optimized = false; if (reduce_vars) { if (node instanceof AST_Toplevel) node.globals.each(reset_def); if (node instanceof AST_Scope) node.variables.each(reset_def); @@ -274,11 +270,11 @@ merge(Compressor.prototype, { var d = node.definition(); d.references.push(node); if (d.fixed === undefined || !is_safe(d) - || is_modified(node, 0, d.fixed instanceof AST_Lambda)) { + || is_modified(node, 0, node.fixed_value() instanceof AST_Lambda)) { d.fixed = false; } } - if (ie8 && node instanceof AST_SymbolCatch) { + if (node instanceof AST_SymbolCatch) { node.definition().fixed = false; } if (node instanceof AST_VarDef) { @@ -287,8 +283,17 @@ merge(Compressor.prototype, { } else { var d = node.name.definition(); if (d.fixed == null) { - d.fixed = node.value; - mark_as_safe(d); + if (node.value) { + d.fixed = function() { + return node.value; + }; + mark(d, false); + descend(); + } else { + d.fixed = null; + } + mark(d, true); + return true; } else if (node.value) { d.fixed = false; } @@ -300,11 +305,10 @@ merge(Compressor.prototype, { d.fixed = false; } else { d.fixed = node; - mark_as_safe(d); + mark(d, true); } var save_ids = safe_ids; - safe_ids = []; - push(); + safe_ids = Object.create(null); descend(); safe_ids = save_ids; return true; @@ -319,8 +323,10 @@ merge(Compressor.prototype, { // So existing transformation rules can work on them. node.argnames.forEach(function(arg, i) { var d = arg.definition(); - d.fixed = iife.args[i] || make_node(AST_Undefined, iife); - mark_as_safe(d); + d.fixed = function() { + return iife.args[i] || make_node(AST_Undefined, iife); + }; + mark(d, true); }); } if (node instanceof AST_If || node instanceof AST_DWLoop) { @@ -368,29 +374,27 @@ merge(Compressor.prototype, { }); this.walk(tw); - function mark_as_safe(def) { - safe_ids[safe_ids.length - 1][def.id] = true; + function mark(def, safe) { + safe_ids[def.id] = safe; } function is_safe(def) { - for (var i = safe_ids.length, id = def.id; --i >= 0;) { - if (safe_ids[i][id]) { - if (def.fixed == null) { - var orig = def.orig[0]; - if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; - def.fixed = make_node(AST_Undefined, orig); - } - return true; + if (safe_ids[def.id]) { + if (def.fixed == null) { + var orig = def.orig[0]; + if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; + def.fixed = make_node(AST_Undefined, orig); } + return true; } } function push() { - safe_ids.push(Object.create(null)); + safe_ids = Object.create(safe_ids); } function pop() { - safe_ids.pop(); + safe_ids = Object.getPrototypeOf(safe_ids); } function reset_def(def) { @@ -405,7 +409,7 @@ merge(Compressor.prototype, { function is_modified(node, level, func) { var parent = tw.parent(level); - if (isLHS(node, parent) + if (is_lhs(node, parent) || !func && parent instanceof AST_Call && parent.expression === node) { return true; } else if (parent instanceof AST_PropAccess && parent.expression === node) { @@ -414,6 +418,12 @@ merge(Compressor.prototype, { } }); + AST_SymbolRef.DEFMETHOD("fixed_value", function() { + var fixed = this.definition().fixed; + if (!fixed || fixed instanceof AST_Node) return fixed; + return fixed(); + }); + function find_variable(compressor, name) { var scope, i = 0; while (scope = compressor.parent(i++)) { @@ -474,15 +484,15 @@ merge(Compressor.prototype, { // func(something) because that changes the meaning of // the func (becomes lexical instead of global). function maintain_this_binding(parent, orig, val) { - if (parent instanceof AST_Call && parent.expression === orig) { - if (val instanceof AST_PropAccess || val instanceof AST_SymbolRef && val.name === "eval") { - return make_node(AST_Seq, orig, { - car: make_node(AST_Number, orig, { - value: 0 - }), - cdr: val - }); - } + if (parent instanceof AST_UnaryPrefix && parent.operator == "delete" + || parent instanceof AST_Call && parent.expression === orig + && (val instanceof AST_PropAccess || val instanceof AST_SymbolRef && val.name == "eval")) { + return make_node(AST_Seq, orig, { + car: make_node(AST_Number, orig, { + value: 0 + }), + cdr: val + }); } return val; } @@ -702,7 +712,7 @@ merge(Compressor.prototype, { return statements; function is_lvalue(node, parent) { - return node instanceof AST_SymbolRef && isLHS(node, parent); + return node instanceof AST_SymbolRef && is_lhs(node, parent); } function replace_var(node, parent, is_constant) { if (is_lvalue(node, parent)) return node; @@ -916,7 +926,7 @@ merge(Compressor.prototype, { } var ab = aborts(stat.body); - var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab.label) : null; + var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab) : null; if (ab && ((ab instanceof AST_Return && !ab.value && in_lambda) || (ab instanceof AST_Continue && self === loop_body(lct)) || (ab instanceof AST_Break && lct instanceof AST_BlockStatement && self === lct))) { @@ -938,7 +948,7 @@ merge(Compressor.prototype, { } var ab = aborts(stat.alternative); - var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab.label) : null; + var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab) : null; if (ab && ((ab instanceof AST_Return && !ab.value && in_lambda) || (ab instanceof AST_Continue && self === loop_body(lct)) || (ab instanceof AST_Break && lct instanceof AST_BlockStatement && self === lct))) { @@ -987,7 +997,7 @@ merge(Compressor.prototype, { extract_declarations_from_unreachable_code(compressor, stat, a); } else { if (stat instanceof AST_LoopControl) { - var lct = compressor.loopcontrol_target(stat.label); + var lct = compressor.loopcontrol_target(stat); if ((stat instanceof AST_Break && !(lct instanceof AST_IterationStatement) && loop_body(lct) === self) || (stat instanceof AST_Continue @@ -1173,6 +1183,61 @@ merge(Compressor.prototype, { && !node.expression.has_side_effects(compressor); } + // may_eq_null() + // returns true if this node may evaluate to null or undefined + (function(def) { + AST_Node.DEFMETHOD("may_eq_null", function(compressor) { + var pure_getters = compressor.option("pure_getters"); + return !pure_getters || this._eq_null(pure_getters); + }); + + function is_strict(pure_getters) { + return /strict/.test(pure_getters); + } + + def(AST_Node, is_strict); + def(AST_Null, return_true); + def(AST_Undefined, return_true); + def(AST_Constant, return_false); + def(AST_Array, return_false); + def(AST_Object, return_false); + def(AST_Function, return_false); + def(AST_UnaryPostfix, return_false); + def(AST_UnaryPrefix, function() { + return this.operator == "void"; + }); + def(AST_Binary, function(pure_getters) { + switch (this.operator) { + case "&&": + return this.left._eq_null(pure_getters); + case "||": + return this.left._eq_null(pure_getters) + && this.right._eq_null(pure_getters); + default: + return false; + } + }) + def(AST_Assign, function(pure_getters) { + return this.operator == "=" + && this.right._eq_null(pure_getters); + }) + def(AST_Conditional, function(pure_getters) { + return this.consequent._eq_null(pure_getters) + || this.alternative._eq_null(pure_getters); + }) + def(AST_Seq, function(pure_getters) { + return this.cdr._eq_null(pure_getters); + }); + def(AST_SymbolRef, function(pure_getters) { + if (this.is_undefined) return true; + if (!is_strict(pure_getters)) return false; + var fixed = this.fixed_value(); + return !fixed || fixed._eq_null(pure_getters); + }); + })(function(node, func) { + node.DEFMETHOD("_eq_null", func); + }); + /* -----[ boolean/negation helpers ]----- */ // methods to determine whether an expression has a boolean result type @@ -1260,9 +1325,9 @@ merge(Compressor.prototype, { var unary_side_effects = makePredicate("delete ++ --"); - function isLHS(node, parent) { - return parent instanceof AST_Unary && unary_side_effects(parent.operator) - || parent instanceof AST_Assign && parent.left === node; + function is_lhs(node, parent) { + if (parent instanceof AST_Unary && unary_side_effects(parent.operator)) return parent.expression; + if (parent instanceof AST_Assign && parent.left === node) return node; } (function (def){ @@ -1275,7 +1340,7 @@ merge(Compressor.prototype, { node = parent; parent = compressor.parent(level++); } while (parent instanceof AST_PropAccess && parent.expression === node); - if (isLHS(node, parent)) { + if (is_lhs(node, parent)) { compressor.warn('global_defs ' + this.print_to_string() + ' redefined [{file}:{line},{col}]', this.start); } else { return def; @@ -1305,7 +1370,7 @@ merge(Compressor.prototype, { } def(AST_Node, noop); def(AST_Dot, function(compressor, suffix){ - return this.expression._find_defs(compressor, suffix + "." + this.property); + return this.expression._find_defs(compressor, "." + this.property + suffix); }); def(AST_SymbolRef, function(compressor, suffix){ if (!this.global()) return; @@ -1525,15 +1590,15 @@ merge(Compressor.prototype, { if (this._evaluating) throw def; this._evaluating = true; try { - var d = this.definition(); - if (compressor.option("reduce_vars") && d.fixed) { + var fixed = this.fixed_value(); + if (compressor.option("reduce_vars") && fixed) { if (compressor.option("unsafe")) { - if (!HOP(d.fixed, "_evaluated")) { - d.fixed._evaluated = ev(d.fixed, compressor); + if (!HOP(fixed, "_evaluated")) { + fixed._evaluated = ev(fixed, compressor); } - return d.fixed._evaluated; + return fixed._evaluated; } - return ev(d.fixed, compressor); + return ev(fixed, compressor); } } finally { this._evaluating = false; @@ -1718,7 +1783,7 @@ merge(Compressor.prototype, { || this.expression.has_side_effects(compressor); }); def(AST_SymbolRef, function(compressor){ - return this.global() && this.undeclared(); + return this.undeclared(); }); def(AST_Object, function(compressor){ return any(this.properties, compressor); @@ -1733,17 +1798,14 @@ merge(Compressor.prototype, { return any(this.elements, compressor); }); def(AST_Dot, function(compressor){ - if (!compressor.option("pure_getters")) return true; - return this.expression.has_side_effects(compressor); + return this.expression.may_eq_null(compressor) + || this.expression.has_side_effects(compressor); }); def(AST_Sub, function(compressor){ - if (!compressor.option("pure_getters")) return true; - return this.expression.has_side_effects(compressor) + return this.expression.may_eq_null(compressor) + || this.expression.has_side_effects(compressor) || this.property.has_side_effects(compressor); }); - def(AST_PropAccess, function(compressor){ - return !compressor.option("pure_getters"); - }); def(AST_Seq, function(compressor){ return this.car.has_side_effects(compressor) || this.cdr.has_side_effects(compressor); @@ -1790,7 +1852,7 @@ merge(Compressor.prototype, { OPT(AST_LabeledStatement, function(self, compressor){ if (self.body instanceof AST_Break - && compressor.loopcontrol_target(self.body.label) === self.body) { + && compressor.loopcontrol_target(self.body) === self.body) { return make_node(AST_EmptyStatement, self); } return self.label.references.length == 0 ? self.body : self; @@ -2388,11 +2450,11 @@ merge(Compressor.prototype, { return values && AST_Seq.from_array(values); }); def(AST_Dot, function(compressor, first_in_statement){ - if (!compressor.option("pure_getters")) return this; + if (this.expression.may_eq_null(compressor)) 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; + if (this.expression.may_eq_null(compressor)) 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); @@ -2438,13 +2500,21 @@ merge(Compressor.prototype, { return make_node(AST_For, self, { body: self.body }); - } else if (compressor.option("dead_code") && self instanceof AST_While) { + } + if (compressor.option("dead_code") && self instanceof AST_While) { var a = []; extract_declarations_from_unreachable_code(compressor, self.body, a); return make_node(AST_BlockStatement, self, { body: a }); - } else { - cond = make_node_from_constant(cond, self.condition).transform(compressor); - self.condition = best_of_expression(cond, self.condition); + } + if (self instanceof AST_Do) { + var has_loop_control = false; + var tw = new TreeWalker(function(node) { + if (node instanceof AST_Scope || has_loop_control) return true; + if (node instanceof AST_LoopControl && tw.loopcontrol_target(node) === self) + return has_loop_control = true; + }); + self.walk(tw); + if (!has_loop_control) return self.body; } } if (self instanceof AST_While) { @@ -2470,7 +2540,7 @@ merge(Compressor.prototype, { var first = self.body instanceof AST_BlockStatement ? self.body.body[0] : self.body; if (first instanceof AST_If) { if (first.body instanceof AST_Break - && compressor.loopcontrol_target(first.body.label) === compressor.self()) { + && compressor.loopcontrol_target(first.body) === compressor.self()) { if (self.condition) { self.condition = make_node(AST_Binary, self.condition, { left: self.condition, @@ -2483,7 +2553,7 @@ merge(Compressor.prototype, { drop_it(first.alternative); } else if (first.alternative instanceof AST_Break - && compressor.loopcontrol_target(first.alternative.label) === compressor.self()) { + && compressor.loopcontrol_target(first.alternative) === compressor.self()) { if (self.condition) { self.condition = make_node(AST_Binary, self.condition, { left: self.condition, @@ -2714,7 +2784,7 @@ merge(Compressor.prototype, { self.body = body; while (branch = body[body.length - 1]) { var stat = branch.body[branch.body.length - 1]; - if (stat instanceof AST_Break && compressor.loopcontrol_target(stat.label) === self) + if (stat instanceof AST_Break && compressor.loopcontrol_target(stat) === self) branch.body.pop(); if (branch.body.length || branch instanceof AST_Case && (default_branch || branch.expression.has_side_effects(compressor))) break; @@ -2733,7 +2803,7 @@ merge(Compressor.prototype, { if (has_break || node instanceof AST_Lambda || node instanceof AST_SimpleStatement) return true; - if (node instanceof AST_Break && tw.loopcontrol_target(node.label) === self) + if (node instanceof AST_Break && tw.loopcontrol_target(node) === self) has_break = true; }); self.walk(tw); @@ -2819,11 +2889,12 @@ merge(Compressor.prototype, { if (compressor.option("reduce_vars") && exp instanceof AST_SymbolRef) { var def = exp.definition(); - if (def.fixed instanceof AST_Defun) { - def.fixed = make_node(AST_Function, def.fixed, def.fixed).clone(true); + var fixed = exp.fixed_value(); + if (fixed instanceof AST_Defun) { + def.fixed = fixed = make_node(AST_Function, fixed, fixed).clone(true); } - if (def.fixed instanceof AST_Function) { - exp = def.fixed; + if (fixed instanceof AST_Function) { + exp = fixed; if (compressor.option("unused") && def.references.length == 1 && !(def.scope.uses_arguments @@ -3158,8 +3229,9 @@ merge(Compressor.prototype, { if (this.expression instanceof AST_Seq) { var seq = this.expression; var x = seq.to_array(); - this.expression = x.pop(); - x.push(this); + var e = this.clone(); + e.expression = x.pop(); + x.push(e); seq = AST_Seq.from_array(x).transform(compressor); return seq; } @@ -3172,11 +3244,27 @@ merge(Compressor.prototype, { }); OPT(AST_UnaryPrefix, function(self, compressor){ + var e = self.expression; + if (self.operator == "delete" + && !(e instanceof AST_SymbolRef + || e instanceof AST_PropAccess + || e instanceof AST_NaN + || e instanceof AST_Infinity + || e instanceof AST_Undefined)) { + if (e instanceof AST_Seq) { + e = e.to_array(); + e.push(make_node(AST_True, self)); + return AST_Seq.from_array(e).optimize(compressor); + } + return make_node(AST_Seq, self, { + car: e, + cdr: make_node(AST_True, self) + }).optimize(compressor); + } var seq = self.lift_sequences(compressor); if (seq !== self) { return seq; } - var e = self.expression; if (compressor.option("side_effects") && self.operator == "void") { e = e.drop_side_effect_free(compressor); if (e) { @@ -3213,9 +3301,14 @@ merge(Compressor.prototype, { if (e instanceof AST_Binary && (self.operator == "+" || self.operator == "-") && (e.operator == "*" || e.operator == "/" || e.operator == "%")) { - self.expression = e.left; - e.left = self; - return e.optimize(compressor); + return make_node(AST_Binary, self, { + operator: e.operator, + left: make_node(AST_UnaryPrefix, e.left, { + operator: self.operator, + expression: e.left + }), + right: e.right + }); } // avoids infinite recursion of numerals if (self.operator != "-" @@ -3234,23 +3327,25 @@ merge(Compressor.prototype, { if (this.left instanceof AST_Seq) { var seq = this.left; var x = seq.to_array(); - this.left = x.pop(); - x.push(this); + var e = this.clone(); + e.left = x.pop(); + x.push(e); return AST_Seq.from_array(x).optimize(compressor); } if (this.right instanceof AST_Seq && !this.left.has_side_effects(compressor)) { var assign = this.operator == "=" && this.left instanceof AST_SymbolRef; - var root = this.right; + var root = this.right.clone(); var cursor, seq = root; while (assign || !seq.car.has_side_effects(compressor)) { cursor = seq; if (seq.cdr instanceof AST_Seq) { - seq = seq.cdr; + seq = seq.cdr = seq.cdr.clone(); } else break; } if (cursor) { - this.right = cursor.cdr; - cursor.cdr = this; + var e = this.clone(); + e.right = cursor.cdr; + cursor.cdr = e; return root.optimize(compressor); } } @@ -3628,12 +3723,11 @@ merge(Compressor.prototype, { OPT(AST_SymbolRef, function(self, compressor){ var def = self.resolve_defines(compressor); if (def) { - return def; + return def.optimize(compressor); } // testing against !self.scope.uses_with first is an optimization if (compressor.option("screw_ie8") && self.undeclared() - && !isLHS(self, compressor.parent()) && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { switch (self.name) { case "undefined": @@ -3646,13 +3740,13 @@ merge(Compressor.prototype, { } if (compressor.option("evaluate") && compressor.option("reduce_vars")) { var d = self.definition(); - if (d.fixed) { + var fixed = self.fixed_value(); + if (fixed) { if (d.should_replace === undefined) { - var init = d.fixed.evaluate(compressor); - if (init !== d.fixed) { - init = make_node_from_constant(init, d.fixed).optimize(compressor); - init = best_of_expression(init, d.fixed); - var value = init.print_to_string().length; + var init = fixed.evaluate(compressor); + if (init !== fixed) { + init = make_node_from_constant(init, fixed); + var value = best_of_expression(init.optimize(compressor), fixed).print_to_string().length; var name = d.name.length; var freq = d.references.length; var overhead = d.global || !freq ? 0 : (name + 2 + value) / freq; @@ -3662,13 +3756,17 @@ merge(Compressor.prototype, { } } if (d.should_replace) { - return d.should_replace.clone(true); + return best_of_expression(d.should_replace.optimize(compressor), fixed).clone(true); } } } return self; }); + function is_atomic(lhs, self) { + return lhs instanceof AST_SymbolRef || lhs.TYPE === self.TYPE; + } + OPT(AST_Undefined, function(self, compressor){ if (compressor.option("unsafe")) { var undef = find_variable(compressor, "undefined"); @@ -3682,6 +3780,8 @@ merge(Compressor.prototype, { return ref; } } + var lhs = is_lhs(compressor.self(), compressor.parent()); + if (lhs && is_atomic(lhs, self)) return self; return make_node(AST_UnaryPrefix, self, { operator: "void", expression: make_node(AST_Number, self, { @@ -3691,8 +3791,13 @@ merge(Compressor.prototype, { }); OPT(AST_Infinity, function(self, compressor){ - var retain = compressor.option("keep_infinity") && !find_variable(compressor, "Infinity"); - return retain ? self : make_node(AST_Binary, self, { + var lhs = is_lhs(compressor.self(), compressor.parent()); + if (lhs && is_atomic(lhs, self)) return self; + if (compressor.option("keep_infinity") + && !(lhs && !is_atomic(lhs, self)) + && !find_variable(compressor, "Infinity")) + return self; + return make_node(AST_Binary, self, { operator: "/", left: make_node(AST_Number, self, { value: 1 @@ -3704,15 +3809,20 @@ merge(Compressor.prototype, { }); OPT(AST_NaN, function(self, compressor){ - return find_variable(compressor, "NaN") ? make_node(AST_Binary, self, { - operator: "/", - left: make_node(AST_Number, self, { - value: 0 - }), - right: make_node(AST_Number, self, { - value: 0 - }) - }) : self; + var lhs = is_lhs(compressor.self(), compressor.parent()); + if (lhs && !is_atomic(lhs, self) + || find_variable(compressor, "NaN")) { + return make_node(AST_Binary, self, { + operator: "/", + left: make_node(AST_Number, self, { + value: 0 + }), + right: make_node(AST_Number, self, { + value: 0 + }) + }); + } + return self; }); var ASSIGN_OPS = [ '+', '-', '/', '*', '%', '>>', '<<', '>>>', '|', '^', '&' ]; @@ -3975,7 +4085,7 @@ merge(Compressor.prototype, { OPT(AST_Dot, function(self, compressor){ var def = self.resolve_defines(compressor); if (def) { - return def; + return def.optimize(compressor); } var prop = self.property; if (RESERVED_WORDS(prop) && !compressor.option("screw_ie8")) { diff --git a/lib/output.js b/lib/output.js index 6c056371..883f58e9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -61,7 +61,7 @@ function OutputStream(options) { ecma : 5, indent_level : 4, indent_start : 0, - inline_script : false, + inline_script : true, keep_quoted_props: false, max_line_len : false, preamble : null, diff --git a/lib/propmangle.js b/lib/propmangle.js index b5b2caaf..e6891085 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -53,7 +53,15 @@ function find_builtins() { objects[new_global] = global[new_global] || new Function(); }); - var a = []; + // NaN will be included due to Number.NaN + var a = [ + "null", + "true", + "false", + "Infinity", + "-Infinity", + "undefined", + ]; [ Object, Array, Function, Number, String, Boolean, Error, Math, Date, RegExp, objects.Symbol, ArrayBuffer, @@ -174,13 +182,12 @@ 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) { return cache.props.has(name); } - if (/^[0-9.]+$/.test(name)) return false; + if (/^-?[0-9]+(\.[0-9]+)?(e[+-][0-9]+)?$/.test(name)) return false; return true; } diff --git a/package.json b/package.json index d9a97f4a..cfa8eb74 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.8.21", + "version": "2.8.22", "engines": { "node": ">=0.8.0" }, diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 07dcb44a..1cded459 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1672,6 +1672,7 @@ var_side_effects_3: { options = { collapse_vars: true, pure_getters: true, + unsafe: true, } input: { var print = console.log.bind(console); diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index e7ea2bb2..200b487f 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -962,3 +962,56 @@ condition_symbol_matches_consequent: { } expect_stdout: "3 7 true 4" } + +delete_conditional_1: { + options = { + booleans: true, + conditionals: true, + evaluate: true, + side_effects: true, + } + input: { + console.log(delete (1 ? undefined : x)); + console.log(delete (1 ? void 0 : x)); + console.log(delete (1 ? Infinity : x)); + console.log(delete (1 ? 1 / 0 : x)); + console.log(delete (1 ? NaN : x)); + console.log(delete (1 ? 0 / 0 : x)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((NaN, !0)); + } + expect_stdout: true +} + +delete_conditional_2: { + options = { + booleans: true, + conditionals: true, + evaluate: true, + keep_infinity: true, + side_effects: true, + } + input: { + console.log(delete (0 ? x : undefined)); + console.log(delete (0 ? x : void 0)); + console.log(delete (0 ? x : Infinity)); + console.log(delete (0 ? x : 1 / 0)); + console.log(delete (0 ? x : NaN)); + console.log(delete (0 ? x : 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((Infinity, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((NaN, !0)); + } + expect_stdout: true +} diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 99e93cfd..fc510718 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -1055,3 +1055,58 @@ issue_1715_4: { } expect_stdout: "1" } + +delete_assign_1: { + options = { + booleans: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a; + console.log(delete (a = undefined)); + console.log(delete (a = void 0)); + console.log(delete (a = Infinity)); + console.log(delete (a = 1 / 0)); + console.log(delete (a = NaN)); + console.log(delete (a = 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_assign_2: { + options = { + booleans: true, + keep_infinity: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a; + console.log(delete (a = undefined)); + console.log(delete (a = void 0)); + console.log(delete (a = Infinity)); + console.log(delete (a = 1 / 0)); + console.log(delete (a = NaN)); + console.log(delete (a = 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((Infinity, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index f43ba9c1..20cbf328 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -951,3 +951,135 @@ issue_1760_2: { } expect_stdout: "Infinity" } + +delete_expr_1: { + options = { + booleans: true, + evaluate: true, + } + input: { + console.log(delete undefined); + console.log(delete void 0); + console.log(delete Infinity); + console.log(delete (1 / 0)); + console.log(delete NaN); + console.log(delete (0 / 0)); + } + expect: { + console.log(delete undefined); + console.log((void 0, !0)); + console.log(delete Infinity); + console.log((1 / 0, !0)); + console.log(delete NaN); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_expr_2: { + options = { + booleans: true, + evaluate: true, + keep_infinity: true, + } + input: { + console.log(delete undefined); + console.log(delete void 0); + console.log(delete Infinity); + console.log(delete (1 / 0)); + console.log(delete NaN); + console.log(delete (0 / 0)); + } + expect: { + console.log(delete undefined); + console.log((void 0, !0)); + console.log(delete Infinity); + console.log((1 / 0, !0)); + console.log(delete NaN); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_binary_1: { + options = { + booleans: true, + evaluate: true, + side_effects: true, + } + input: { + console.log(delete (true && undefined)); + console.log(delete (true && void 0)); + console.log(delete (true && Infinity)); + console.log(delete (true && (1 / 0))); + console.log(delete (true && NaN)); + console.log(delete (true && (0 / 0))); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((NaN, !0)); + } + expect_stdout: true +} + +delete_binary_2: { + options = { + booleans: true, + evaluate: true, + keep_infinity: true, + side_effects: true, + } + input: { + console.log(delete (false || undefined)); + console.log(delete (false || void 0)); + console.log(delete (false || Infinity)); + console.log(delete (false || (1 / 0))); + console.log(delete (false || NaN)); + console.log(delete (false || (0 / 0))); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((Infinity, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((NaN, !0)); + } + expect_stdout: true +} + +Infinity_NaN_undefined_LHS: { + beautify = { + beautify: true, + } + input: { + function f() { + Infinity = Infinity; + ++Infinity; + Infinity--; + NaN *= NaN; + ++NaN; + NaN--; + undefined |= undefined; + ++undefined; + undefined--; + } + } + expect_exact: [ + "function f() {", + " Infinity = 1 / 0;", + " ++Infinity;", + " Infinity--;", + " NaN *= NaN;", + " ++NaN;", + " NaN--;", + " undefined |= void 0;", + " ++undefined;", + " undefined--;", + "}", + ] +} diff --git a/test/compress/global_defs.js b/test/compress/global_defs.js index a69d031e..f1ba8f32 100644 --- a/test/compress/global_defs.js +++ b/test/compress/global_defs.js @@ -145,3 +145,18 @@ mixed: { 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:129,8]', ] } + +issue_1801: { + options = { + booleans: true, + global_defs: { + "CONFIG.FOO.BAR": true, + }, + } + input: { + console.log(CONFIG.FOO.BAR); + } + expect: { + console.log(!0); + } +} diff --git a/test/compress/issue-1609.js b/test/compress/issue-1609.js index 577a3ee1..da4b54a2 100644 --- a/test/compress/issue-1609.js +++ b/test/compress/issue-1609.js @@ -45,11 +45,10 @@ chained_evaluation_2: { } expect: { (function() { - var a = "long piece of string"; (function() { - var c; - c = f(a); - c.bar = a; + var c, b = "long piece of string"; + c = f(b); + c.bar = b; })(); })(); } diff --git a/test/compress/issue-1770.js b/test/compress/issue-1770.js new file mode 100644 index 00000000..de0d4dd3 --- /dev/null +++ b/test/compress/issue-1770.js @@ -0,0 +1,105 @@ +mangle_props: { + mangle_props = {} + input: { + var obj = { + undefined: 1, + NaN: 2, + Infinity: 3, + "-Infinity": 4, + null: 5, + }; + console.log( + obj[void 0], + obj[undefined], + obj["undefined"], + obj[0/0], + obj[NaN], + obj["NaN"], + obj[1/0], + obj[Infinity], + obj["Infinity"], + obj[-1/0], + obj[-Infinity], + obj["-Infinity"], + obj[null], + obj["null"] + ); + } + expect: { + var obj = { + undefined: 1, + NaN: 2, + Infinity: 3, + "-Infinity": 4, + null: 5, + }; + console.log( + obj[void 0], + obj[void 0], + obj["undefined"], + obj[0/0], + obj[NaN], + obj["NaN"], + obj[1/0], + obj[1/0], + obj["Infinity"], + obj[-1/0], + obj[-1/0], + obj["-Infinity"], + obj[null], + obj["null"] + ); + } + expect_stdout: "1 1 1 2 2 2 3 3 3 4 4 4 5 5" +} + +numeric_literal: { + beautify = { + beautify: true, + } + mangle_props = {} + input: { + var obj = { + 0: 0, + "-0": 1, + 42: 2, + "42": 3, + 0x25: 4, + "0x25": 5, + 1E42: 6, + "1E42": 7, + "1e+42": 8, + }; + console.log(obj[-0], obj[-""], obj["-0"]); + console.log(obj[42], obj["42"]); + console.log(obj[0x25], obj["0x25"], obj[37], obj["37"]); + console.log(obj[1E42], obj["1E42"], obj["1e+42"]); + } + expect_exact: [ + 'var obj = {', + ' 0: 0,', + ' "-0": 1,', + ' 42: 2,', + ' "42": 3,', + ' 37: 4,', + ' a: 5,', + ' 1e42: 6,', + ' b: 7,', + ' "1e+42": 8', + '};', + '', + 'console.log(obj[-0], obj[-""], obj["-0"]);', + '', + 'console.log(obj[42], obj["42"]);', + '', + 'console.log(obj[37], obj["a"], obj[37], obj["37"]);', + '', + 'console.log(obj[1e42], obj["b"], obj["1e+42"]);', + ] + expect_stdout: [ + "0 0 1", + "3 3", + "4 5 4 4", + "8 7 8", + ] +} diff --git a/test/compress/issue-1787.js b/test/compress/issue-1787.js new file mode 100644 index 00000000..43d1f1be --- /dev/null +++ b/test/compress/issue-1787.js @@ -0,0 +1,19 @@ +unary_prefix: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + console.log(function() { + var x = -(2 / 3); + return x; + }()); + } + expect: { + console.log(function() { + return -2 / 3; + }()); + } + expect_stdout: true +} diff --git a/test/compress/loops.js b/test/compress/loops.js index c8d77840..f13f5cc5 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -215,8 +215,7 @@ evaluate: { a(); for(;;) c(); - // rule disabled due to issue_1532 - do d(); while (false); + d(); } } @@ -458,3 +457,26 @@ issue_1648: { } expect_exact: "function f(){for(x();1;);}" } + +do_switch: { + options = { + evaluate: true, + loops: true, + } + input: { + do { + switch (a) { + case b: + continue; + } + } while (false); + } + expect: { + do { + switch (a) { + case b: + continue; + } + } while (false); + } +} diff --git a/test/compress/pure_getters.js b/test/compress/pure_getters.js new file mode 100644 index 00000000..846b53c3 --- /dev/null +++ b/test/compress/pure_getters.js @@ -0,0 +1,121 @@ +strict: { + options = { + pure_getters: "strict", + reduce_vars: false, + side_effects: true, + toplevel: true, + } + input: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + undefined.prop; + } + expect: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + (void 0).prop; + } +} + +strict_reduce_vars: { + options = { + pure_getters: "strict", + reduce_vars: true, + side_effects: true, + toplevel: true, + } + input: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + undefined.prop; + } + expect: { + var a, b = null, c = {}; + a.prop; + b.prop; + d.prop; + null.prop; + (void 0).prop; + (void 0).prop; + } +} + +unsafe: { + options = { + pure_getters: true, + reduce_vars: false, + side_effects: true, + toplevel: true, + } + input: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + undefined.prop; + } + expect: { + var a, b = null, c = {}; + d; + null.prop; + (void 0).prop; + (void 0).prop; + } +} + +unsafe_reduce_vars: { + options = { + pure_getters: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + } + input: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + undefined.prop; + } + expect: { + var a, b = null, c = {}; + d; + null.prop; + (void 0).prop; + (void 0).prop; + } +} + +chained: { + options = { + pure_getters: "strict", + side_effects: true, + } + input: { + a.b.c; + } + expect: { + a.b.c; + } +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index cdc4ef20..b6f711ad 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1866,3 +1866,132 @@ delay_def: { } expect_stdout: true } + +booleans: { + options = { + booleans: true, + evaluate: true, + reduce_vars: true, + } + input: { + console.log(function(a) { + if (a != 0); + switch (a) { + case 0: + return "FAIL"; + case false: + return "PASS"; + } + }(false)); + } + expect: { + console.log(function(a) { + if (!1); + switch (!1) { + case 0: + return "FAIL"; + case !1: + return "PASS"; + } + }(!1)); + } + expect_stdout: "PASS" +} + +side_effects_assign: { + options = { + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + toplevel: true, + } + input: { + var a = typeof void (a && a.in == 1, 0); + console.log(a); + } + expect: { + var a = typeof void (a && a.in); + console.log(a); + } + expect_stdout: "undefined" +} + +pure_getters_1: { + options = { + pure_getters: "strict", + reduce_vars: true, + side_effects: true, + toplevel: true, + } + input: { + try { + var a = (a.b, 2); + } catch (e) {} + console.log(a); + } + expect: { + try { + var a = (a.b, 2); + } catch (e) {} + console.log(a); + } + expect_stdout: "undefined" +} + +pure_getters_2: { + options = { + pure_getters: "strict", + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var a; + var a = a && a.b; + } + expect: { + var a; + var a = a && a.b; + } +} + +pure_getters_3: { + options = { + pure_getters: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var a; + var a = a && a.b; + } + expect: { + } +} + +catch_var: { + options = { + booleans: true, + evaluate: true, + reduce_vars: true, + } + input: { + try { + throw {}; + } catch (e) { + var e; + console.log(!!e); + } + } + expect: { + try { + throw {}; + } catch (e) { + var e; + console.log(!!e); + } + } + expect_stdout: "true" +} diff --git a/test/compress/sequences.js b/test/compress/sequences.js index b3c54635..699341c0 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -466,3 +466,147 @@ issue_1758: { } expect_stdout: "undefined" } + +delete_seq_1: { + options = { + booleans: true, + side_effects: true, + } + input: { + console.log(delete (1, undefined)); + console.log(delete (1, void 0)); + console.log(delete (1, Infinity)); + console.log(delete (1, 1 / 0)); + console.log(delete (1, NaN)); + console.log(delete (1, 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_seq_2: { + options = { + booleans: true, + side_effects: true, + } + input: { + console.log(delete (1, 2, undefined)); + console.log(delete (1, 2, void 0)); + console.log(delete (1, 2, Infinity)); + console.log(delete (1, 2, 1 / 0)); + console.log(delete (1, 2, NaN)); + console.log(delete (1, 2, 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_seq_3: { + options = { + booleans: true, + keep_infinity: true, + side_effects: true, + } + input: { + console.log(delete (1, 2, undefined)); + console.log(delete (1, 2, void 0)); + console.log(delete (1, 2, Infinity)); + console.log(delete (1, 2, 1 / 0)); + console.log(delete (1, 2, NaN)); + console.log(delete (1, 2, 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((Infinity, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_seq_4: { + options = { + booleans: true, + sequences: true, + side_effects: true, + } + input: { + function f() {} + console.log(delete (f(), undefined)); + console.log(delete (f(), void 0)); + console.log(delete (f(), Infinity)); + console.log(delete (f(), 1 / 0)); + console.log(delete (f(), NaN)); + console.log(delete (f(), 0 / 0)); + } + expect: { + function f() {} + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)); + } + expect_stdout: true +} + +delete_seq_5: { + options = { + booleans: true, + keep_infinity: true, + sequences: true, + side_effects: true, + } + input: { + function f() {} + console.log(delete (f(), undefined)); + console.log(delete (f(), void 0)); + console.log(delete (f(), Infinity)); + console.log(delete (f(), 1 / 0)); + console.log(delete (f(), NaN)); + console.log(delete (f(), 0 / 0)); + } + expect: { + function f() {} + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)); + } + expect_stdout: true +} + +delete_seq_6: { + options = { + booleans: true, + side_effects: true, + } + input: { + var a; + console.log(delete (1, a)); + } + expect: { + var a; + console.log((a, !0)); + } + expect_stdout: true +} diff --git a/test/sandbox.js b/test/sandbox.js index 504e5e25..894349fb 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -12,7 +12,6 @@ var FUNC_TOSTRING = [ ' return "[Function: __func_" + i + "__]";', " }", "}();", - "" ].join("\n"); exports.run_code = function(code) { var stdout = ""; @@ -21,15 +20,20 @@ exports.run_code = function(code) { stdout += chunk; }; try { - new vm.Script(FUNC_TOSTRING + code).runInNewContext({ + vm.runInNewContext([ + "!function() {", + FUNC_TOSTRING, + code, + "}();", + ].join("\n"), { console: { log: function() { return console.log.apply(console, [].map.call(arguments, function(arg) { - return typeof arg == "function" ? arg.toString() : arg; + return typeof arg == "function" || arg && /Error$/.test(arg.name) ? arg.toString() : arg; })); } } - }, { timeout: 30000 }); + }, { timeout: 5000 }); return stdout; } catch (ex) { return ex; diff --git a/test/ufuzz.js b/test/ufuzz.js index 7aab6715..2a09e2f7 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -71,6 +71,7 @@ var STMT_COUNT_FROM_GLOBAL = true; // count statement depth from nearest functio var num_iterations = +process.argv[2] || 1/0; var verbose = false; // log every generated test var verbose_interval = false; // log every 100 generated tests +var verbose_error = false; for (var i = 2; i < process.argv.length; ++i) { switch (process.argv[i]) { case '-v': @@ -79,6 +80,9 @@ for (var i = 2; i < process.argv.length; ++i) { case '-V': verbose_interval = true; break; + case '-E': + verbose_error = true; + break; case '-t': MAX_GENERATED_TOPLEVELS_PER_RUN = +process.argv[++i]; if (!MAX_GENERATED_TOPLEVELS_PER_RUN) throw new Error('Must generate at least one toplevel per run'); @@ -118,6 +122,7 @@ for (var i = 2; i < process.argv.length; ++i) { console.log(': generate this many cases (if used must be first arg)'); console.log('-v: print every generated test case'); console.log('-V: print every 100th generated test case'); + console.log('-E: print generated test case with runtime error'); console.log('-t : generate this many toplevels per run (more take longer)'); console.log('-r : maximum recursion depth for generator (higher takes longer)'); console.log('-s1 : force the first level statement to be this one (see list below)'); @@ -135,6 +140,7 @@ for (var i = 2; i < process.argv.length; ++i) { } var VALUES = [ + '""', 'true', 'false', ' /[a2][^e]+$/ ', @@ -221,15 +227,19 @@ var ASSIGNMENTS = [ '>>>=', '%=' ]; -var UNARY_OPS = [ - '--', - '++', +var UNARY_SAFE = [ + '+', + '-', '~', '!', 'void ', - 'delete ', // should be safe, even `delete foo` and `delete f()` shouldn't crash - ' - ', - ' + ' ]; + 'delete ', +]; +var UNARY_POSTFIX = [ + '++', + '--', +]; +var UNARY_PREFIX = UNARY_POSTFIX.concat(UNARY_SAFE); var NO_COMMA = true; var COMMA_OK = false; @@ -250,26 +260,26 @@ var NO_DECL = true; var DONT_STORE = true; var VAR_NAMES = [ - 'foo', - 'bar', + 'a', + 'a', + 'a', 'a', 'b', + 'b', + 'b', + 'b', 'c', // prevent redeclaring this, avoid assigning to this - 'undefined', // fun! - 'eval', // mmmm, ok, also fun! - 'NaN', // mmmm, ok, also fun! - 'Infinity', // the fun never ends! - 'arguments', // this one is just creepy - 'Math', // since Math is assumed to be a non-constructor/function it may trip certain cases + 'foo', + 'foo', + 'bar', + 'bar', + 'undefined', + 'NaN', + 'Infinity', + 'arguments', + 'Math', 'parseInt', - 'parseFloat', - 'isNaN', - 'isFinite', - 'decodeURI', - 'decodeURIComponent', - 'encodeURI', - 'encodeURIComponent', - 'Object']; +]; var INITIAL_NAMES_LEN = VAR_NAMES.length; var TYPEOF_OUTCOMES = [ @@ -306,6 +316,22 @@ function createFunctions(n, recurmax, inGlobal, noDecl, canThrow, stmtDepth) { return s; } +function createParams() { + var params = []; + for (var n = rng(4); --n >= 0;) { + params.push(createVarName(MANDATORY)); + } + return params.join(', '); +} + +function createArgs() { + var args = []; + for (var n = rng(4); --n >= 0;) { + args.push(createValue()); + } + return args.join(', '); +} + function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { if (--recurmax < 0) { return ';'; } if (!STMT_COUNT_FROM_GLOBAL) stmtDepth = 0; @@ -316,17 +342,17 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { var s = ''; if (rng(5) === 0) { // functions with functions. lower the recursion to prevent a mess. - s = 'function ' + name + '(' + createVarName(MANDATORY) + '){' + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth) + '}\n'; + s = 'function ' + name + '(' + createParams() + '){' + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth) + '}\n'; } else { // functions with statements - s = 'function ' + name + '(' + createVarName(MANDATORY) + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; + s = 'function ' + name + '(' + createParams() + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; } VAR_NAMES.length = namesLenBefore; - if (noDecl) s = '!' + s + '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; + if (noDecl) s = 'var ' + createVarName(MANDATORY) + ' = ' + s + '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ');'; // avoid "function statements" (decl inside statements) - else if (inGlobal || rng(10) > 0) s += name + '();' + else if (inGlobal || rng(10) > 0) s += 'var ' + createVarName(MANDATORY) + ' = ' + name + '(' + createArgs() + ');'; return s; @@ -393,23 +419,27 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn return 'var ' + n1 + ' = ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ', ' + n2 + ' = ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; } case STMT_RETURN_ETC: - switch (rng(3)) { + switch (rng(8)) { + case 0: case 1: + case 2: + case 3: if (canBreak && rng(5) === 0) return 'break;'; if (canContinue && rng(5) === 0) return 'continue;'; if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; - return '/*3*/return;'; - case 2: - // must wrap in curlies to prevent orphaned `else` statement - if (canThrow && rng(5) === 0) return '{ throw ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; - if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; - return '{ /*1*/ return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; - default: + if (rng(3) == 0) return '/*3*/return;'; + return 'return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; + case 4: // this is actually more like a parser test, but perhaps it hits some dead code elimination traps // must wrap in curlies to prevent orphaned `else` statement // note: you can't `throw` without an expression so don't put a `throw` option in this case if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; return '{ /*2*/ return\n' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; + default: + // must wrap in curlies to prevent orphaned `else` statement + if (canThrow && rng(5) === 0) return '{ throw ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; + if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; + return '{ return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; } case STMT_FUNC_EXPR: // "In non-strict mode code, functions can only be declared at top level, inside a block, or ..." @@ -463,36 +493,44 @@ function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotR function createExpression(recurmax, noComma, stmtDepth, canThrow) { if (--recurmax < 0) { - return '(c = 1 + c, ' + createNestedBinaryExpr(recurmax, noComma) + ')'; // note: should return a simple non-recursing expression value! + return '(c = 1 + c, ' + createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; // note: should return a simple non-recursing expression value! } // since `a` and `b` are our canaries we want them more frequently than other expressions (1/3rd chance of a canary) - var r = rng(6); - if (r < 1) return 'a++ + ' + _createExpression(recurmax, noComma, stmtDepth, canThrow); - if (r < 2) return '(--b) + ' + _createExpression(recurmax, noComma, stmtDepth, canThrow); - if (r < 3) return '(c = c + 1) + ' + _createExpression(recurmax, noComma, stmtDepth, canThrow); // c only gets incremented - - return _createExpression(recurmax, noComma, stmtDepth, canThrow); + switch (rng(6)) { + case 0: + return '(a++ + (' + _createExpression(recurmax, noComma, stmtDepth, canThrow) + '))'; + case 1: + return '((--b) + (' + _createExpression(recurmax, noComma, stmtDepth, canThrow) + '))'; + case 2: + return '((c = c + 1) + (' + _createExpression(recurmax, noComma, stmtDepth, canThrow) + '))'; // c only gets incremented + default: + return '(' + _createExpression(recurmax, noComma, stmtDepth, canThrow) + ')'; + } } function _createExpression(recurmax, noComma, stmtDepth, canThrow) { - switch (rng(15)) { - case 0: - return createUnaryOp() + (rng(2) === 1 ? 'a' : 'b'); - case 1: - return 'a' + (rng(2) == 1 ? '++' : '--'); - case 2: + var p = 0; + switch (rng(_createExpression.N)) { + case p++: + case p++: + return createUnaryPrefix() + (rng(2) === 1 ? 'a' : 'b'); + case p++: + case p++: + return (rng(2) === 1 ? 'a' : 'b') + createUnaryPostfix(); + case p++: + case p++: // parens needed because assignments aren't valid unless they're the left-most op(s) in an expression - return '(b ' + createAssignment() + ' a)'; - case 3: + return 'b ' + createAssignment() + ' a'; + case p++: + case p++: return rng(2) + ' === 1 ? a : b'; - case 4: - return createNestedBinaryExpr(recurmax, noComma) + createBinaryOp(noComma) + createExpression(recurmax, noComma, stmtDepth, canThrow); - case 5: + case p++: + case p++: return createValue(); - case 6: - return '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; - case 7: + case p++: + return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow); + case p++: return createExpression(recurmax, noComma, stmtDepth, canThrow) + '?' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':' + createExpression(recurmax, noComma, stmtDepth, canThrow); - case 8: + case p++: var nameLenBefore = VAR_NAMES.length; var name = createVarName(MAYBE); // note: this name is only accessible from _within_ the function. and immutable at that. if (name === 'c') name = 'a'; @@ -513,17 +551,18 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { } VAR_NAMES.length = nameLenBefore; return s; - case 9: + case p++: + case p++: return createTypeofExpr(recurmax, stmtDepth, canThrow); - case 10: - // you could statically infer that this is just `Math`, regardless of the other expression - // I don't think Uglify does this at this time... - return ''+ - 'new function(){ \n' + - (rng(2) === 1 ? createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + '\n' : '') + - 'return Math;\n' + - '}'; - case 11: + case p++: + return [ + 'new function() {', + rng(2) ? '' : createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';', + 'return ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';', + '}' + ].join('\n'); + case p++: + case p++: // more like a parser test but perhaps comment nodes mess up the analysis? // note: parens not needed for post-fix (since that's the default when ambiguous) // for prefix ops we need parens to prevent accidental syntax errors. @@ -533,58 +572,151 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { case 1: return 'b/* ignore */--'; case 2: - return '(++/* ignore */a)'; + return '++/* ignore */a'; case 3: - return '(--/* ignore */b)'; + return '--/* ignore */b'; case 4: // only groups that wrap a single variable return a "Reference", so this is still valid. // may just be a parser edge case that is invisible to uglify... - return '(--(b))'; + return '--(b)'; case 5: // classic 0.3-0.1 case; 1-0.1-0.1-0.1 is not 0.7 :) return 'b + 1-0.1-0.1-0.1'; default: - return '(--/* ignore */b)'; + return '--/* ignore */b'; } - case 12: - return createNestedBinaryExpr(recurmax, noComma); - case 13: + case p++: + case p++: + return createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow); + case p++: + case p++: + return createUnarySafePrefix() + '(' + createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + case p++: return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || a || 3).toString() "; - case 14: + case p++: return " /[abc4]/.test(((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || b || 5).toString()) "; + case p++: + return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + + ") || " + rng(10) + ").toString()[" + + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "] "; + case p++: + return createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow); + case p++: + return createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow); + case p++: + return createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '[' + + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']'; + case p++: + return createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '[' + + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']'; + case p++: + return createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '.' + getDotKey(); + case p++: + return createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '.' + getDotKey(); + case p++: + var name = getVarName(); + return name + ' && ' + name + '[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']'; + case p++: + var name = getVarName(); + return name + ' && ' + name + '.' + getDotKey(); } + _createExpression.N = p; + return _createExpression(recurmax, noComma, stmtDepth, canThrow); } -function createNestedBinaryExpr(recurmax, noComma) { - recurmax = 3; // note that this generates 2^recurmax expression parts... make sure to cap it - return _createSimpleBinaryExpr(recurmax, noComma); +function createArrayLiteral(recurmax, noComma, stmtDepth, canThrow) { + recurmax--; + var arr = "["; + for (var i = rng(6); --i >= 0;) { + // in rare cases produce an array hole element + var element = rng(20) ? createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) : ""; + arr += element + ", "; + } + return arr + "]"; } -function _createSimpleBinaryExpr(recurmax, noComma) { + +var SAFE_KEYS = [ + "length", + "foo", + "a", + "b", + "c", + "undefined", + "null", + "NaN", + "Infinity", + "in", + "var", +]; +var KEYS = [ + "''", + '"\t"', + '"-2"', + "0", + "1.5", + "3", +].concat(SAFE_KEYS); + +function getDotKey() { + return SAFE_KEYS[rng(SAFE_KEYS.length)]; +} + +function createObjectLiteral(recurmax, noComma, stmtDepth, canThrow) { + recurmax--; + var obj = "({"; + for (var i = rng(6); --i >= 0;) { + var key = KEYS[rng(KEYS.length)]; + obj += key + ":(" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "), "; + } + return obj + "})"; +} + +function createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { + recurmax = 3; // note that this generates 2^recurmax expression parts... make sure to cap it + return _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow); +} +function _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { + return '(' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + + createBinaryOp(noComma) + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; +} +function _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { // intentionally generate more hardcore ops if (--recurmax < 0) return createValue(); - var r = rng(30); - if (r === 0) return '(c = c + 1, ' + _createSimpleBinaryExpr(recurmax, noComma) + ')'; - var s = _createSimpleBinaryExpr(recurmax, noComma) + createBinaryOp(noComma) + _createSimpleBinaryExpr(recurmax, noComma); - if (r === 1) { - // try to get a generated name reachable from current scope. default to just `a` - var assignee = VAR_NAMES[INITIAL_NAMES_LEN + rng(VAR_NAMES.length - INITIAL_NAMES_LEN)] || 'a'; - return '( ' + assignee + createAssignment() + s + ')'; + switch (rng(30)) { + case 0: + return '(c = c + 1, ' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + case 1: + return '(' + createUnarySafePrefix() + '(' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + '))'; + case 2: + var assignee = getVarName(); + return '(' + assignee + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + case 3: + var assignee = getVarName(); + var expr = '(' + assignee + '[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + + ']' + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + return canThrow && rng(10) == 0 ? expr : '(' + assignee + ' && ' + expr + ')'; + case 4: + var assignee = getVarName(); + var expr = '(' + assignee + '.' + getDotKey() + createAssignment() + + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + return canThrow && rng(10) == 0 ? expr : '(' + assignee + ' && ' + expr + ')'; + default: + return _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow); } - return s; } function createTypeofExpr(recurmax, stmtDepth, canThrow) { switch (rng(8)) { case 0: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; case 1: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; case 2: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; case 3: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; case 4: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE); + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ')'; default: return '(typeof ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; } @@ -603,16 +735,31 @@ function createAssignment() { return ASSIGNMENTS[rng(ASSIGNMENTS.length)]; } -function createUnaryOp() { - return UNARY_OPS[rng(UNARY_OPS.length)]; +function createUnarySafePrefix() { + return UNARY_SAFE[rng(UNARY_SAFE.length)]; +} + +function createUnaryPrefix() { + return UNARY_PREFIX[rng(UNARY_PREFIX.length)]; +} + +function createUnaryPostfix() { + return UNARY_POSTFIX[rng(UNARY_POSTFIX.length)]; +} + +function getVarName() { + // try to get a generated name reachable from current scope. default to just `a` + return VAR_NAMES[INITIAL_NAMES_LEN + rng(VAR_NAMES.length - INITIAL_NAMES_LEN)] || 'a'; } function createVarName(maybe, dontStore) { - if (!maybe || rng(2) === 1) { - var r = rng(VAR_NAMES.length); - var suffixed = rng(5) > 0; - var name = VAR_NAMES[r] + (suffixed ? '_' + (++loops) : ''); - if (!dontStore && suffixed) VAR_NAMES.push(name); + if (!maybe || rng(2)) { + var name = VAR_NAMES[rng(VAR_NAMES.length)]; + var suffix = rng(3); + if (suffix) { + name += '_' + suffix; + if (!dontStore) VAR_NAMES.push(name); + } return name; } return ''; @@ -755,6 +902,20 @@ for (var round = 1; round <= num_iterations; round++) { ok = sandbox.same_stdout(original_result, uglify_result); } if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(options); - if (!ok && isFinite(num_iterations)) process.exit(1); + else if (verbose_error && typeof original_result != "string") { + console.log("//============================================================="); + console.log("// original code"); + try_beautify(original_code, original_result); + console.log(); + console.log(); + console.log("original result:"); + console.log(original_result); + console.log(); + } + if (!ok && isFinite(num_iterations)) { + console.log(); + process.exit(1); + } }); } +console.log(); diff --git a/test/ufuzz.json b/test/ufuzz.json index 2d871e87..c4813470 100644 --- a/test/ufuzz.json +++ b/test/ufuzz.json @@ -1,19 +1,4 @@ [ - { - "compress": { - "warnings": false - } - }, - { - "compress": { - "warnings": false - }, - "mangle": false - }, - { - "compress": false, - "mangle": true - }, { "compress": false, "mangle": false, @@ -22,11 +7,33 @@ "bracketize": true } }, + { + "compress": false + }, + { + "compress": { + "warnings": false + }, + "mangle": false + }, + { + "compress": { + "warnings": false + } + }, + { + "compress": { + "toplevel": true, + "warnings": false + }, + "mangle": { + "toplevel": true + } + }, { "compress": { "keep_fargs": false, "passes": 3, - "pure_getters": true, "warnings": false } }