diff --git a/README.md b/README.md index d4a624a1..9e31d0f6 100644 --- a/README.md +++ b/README.md @@ -443,6 +443,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). integer argument larger than 1 to further reduce code size in some cases. Note: raising the number of passes will increase uglify compress time. +- `keep_infinity` -- default `false`. Pass `true` to prevent `Infinity` from + being compressed into `1/0`, which may cause performance issues on Chrome. + ### The `unsafe` option It enables some transformations that *might* break code logic in certain diff --git a/lib/compress.js b/lib/compress.js index af08a664..76785f46 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -48,43 +48,45 @@ function Compressor(options, false_by_default) { return new Compressor(options, false_by_default); TreeTransformer.call(this, this.before, this.after); this.options = defaults(options, { - sequences : !false_by_default, - properties : !false_by_default, + angular : false, + booleans : !false_by_default, + cascade : !false_by_default, + collapse_vars : !false_by_default, + comparisons : !false_by_default, + conditionals : !false_by_default, dead_code : !false_by_default, + drop_console : false, drop_debugger : !false_by_default, + ecma : 5, + evaluate : !false_by_default, + expression : false, + global_defs : {}, + hoist_funs : !false_by_default, + hoist_vars : false, + if_return : !false_by_default, + join_vars : !false_by_default, + keep_fargs : true, + keep_fnames : false, + keep_infinity : false, + loops : !false_by_default, + negate_iife : !false_by_default, + passes : 1, + properties : !false_by_default, + pure_getters : false, + pure_funcs : null, + reduce_vars : !false_by_default, + screw_ie8 : true, + sequences : !false_by_default, + side_effects : !false_by_default, + switches : !false_by_default, + top_retain : null, + toplevel : !!(options && options["top_retain"]), unsafe : false, unsafe_comps : false, unsafe_math : false, unsafe_proto : false, - conditionals : !false_by_default, - comparisons : !false_by_default, - evaluate : !false_by_default, - booleans : !false_by_default, - loops : !false_by_default, unused : !false_by_default, - toplevel : !!(options && options["top_retain"]), - top_retain : null, - hoist_funs : !false_by_default, - keep_fargs : true, - keep_fnames : false, - hoist_vars : false, - if_return : !false_by_default, - join_vars : !false_by_default, - collapse_vars : !false_by_default, - reduce_vars : !false_by_default, - cascade : !false_by_default, - side_effects : !false_by_default, - pure_getters : false, - pure_funcs : null, - negate_iife : !false_by_default, - screw_ie8 : true, - ecma : 5, - drop_console : false, - angular : false, - expression : false, warnings : true, - global_defs : {}, - passes : 1, }, true); var pure_funcs = this.options["pure_funcs"]; if (typeof pure_funcs == "function") { @@ -216,7 +218,12 @@ merge(Compressor.prototype, { }) : make_node(AST_EmptyStatement, node); } return make_node(AST_SimpleStatement, node, { - body: node.value || make_node(AST_Undefined, node) + body: node.value || make_node(AST_UnaryPrefix, node, { + operator: "void", + expression: make_node(AST_Number, node, { + value: 0 + }) + }) }); } if (node instanceof AST_Lambda && node !== self) { @@ -407,6 +414,18 @@ merge(Compressor.prototype, { } }); + function find_variable(compressor, name) { + var scope, i = 0; + while (scope = compressor.parent(i++)) { + if (scope instanceof AST_Scope) break; + if (scope instanceof AST_Catch) { + scope = scope.argname.definition().scope; + break; + } + } + return scope.find_variable(name); + } + function make_node(ctor, orig, props) { if (!props) props = {}; if (orig) { @@ -1071,7 +1090,7 @@ merge(Compressor.prototype, { stat.value = cons_seq(stat.value); } else if (stat instanceof AST_Exit) { - stat.value = cons_seq(make_node(AST_Undefined, stat)); + stat.value = cons_seq(make_node(AST_Undefined, stat).transform(compressor)); } else if (stat instanceof AST_Switch) { stat.expression = cons_seq(stat.expression); @@ -1146,8 +1165,12 @@ merge(Compressor.prototype, { })); }; - function is_undefined(node) { - return node instanceof AST_Undefined || node.is_undefined; + function is_undefined(node, compressor) { + return node.is_undefined + || node instanceof AST_Undefined + || node instanceof AST_UnaryPrefix + && node.operator == "void" + && !node.expression.has_side_effects(compressor); } /* -----[ boolean/negation helpers ]----- */ @@ -1339,7 +1362,7 @@ merge(Compressor.prototype, { return this; } }); - var unaryPrefix = makePredicate("! ~ - +"); + var unaryPrefix = makePredicate("! ~ - + void"); AST_Node.DEFMETHOD("is_constant", function(){ // Accomodate when compress option evaluate=false // as well as the common constant expressions !0 and -1 @@ -2638,6 +2661,7 @@ merge(Compressor.prototype, { }); OPT(AST_Switch, function(self, compressor){ + if (!compressor.option("switches")) return self; var branch; var value = self.expression.evaluate(compressor); if (value !== self.expression) { @@ -2649,49 +2673,39 @@ merge(Compressor.prototype, { var body = []; var default_branch; var exact_match; - var fallthrough; for (var i = 0, len = self.body.length; i < len && !exact_match; i++) { branch = self.body[i]; if (branch instanceof AST_Default) { - if (!default_branch) default_branch = branch; - else if (!fallthrough) { - extract_declarations_from_unreachable_code(compressor, branch, decl); - continue; + if (!default_branch) { + default_branch = branch; + } else { + eliminate_branch(branch, body[body.length - 1]); } } else if (value !== self.expression) { var exp = branch.expression.evaluate(compressor); if (exp === value) { exact_match = branch; if (default_branch) { - body.splice(body.indexOf(default_branch), 1); - extract_declarations_from_unreachable_code(compressor, default_branch, decl); + var default_index = body.indexOf(default_branch); + body.splice(default_index, 1); + eliminate_branch(default_branch, body[default_index - 1]); default_branch = null; } - } else if (exp !== branch.expression && !fallthrough) { - extract_declarations_from_unreachable_code(compressor, branch, decl); + } else if (exp !== branch.expression) { + eliminate_branch(branch, body[body.length - 1]); continue; } } if (aborts(branch)) { - if (body.length > 0 && !fallthrough) { - var prev = body[body.length - 1]; - if (prev.body.length == branch.body.length - && make_node(AST_BlockStatement, prev, prev).equivalent_to(make_node(AST_BlockStatement, branch, branch))) - prev.body = []; + var prev = body[body.length - 1]; + if (aborts(prev) && prev.body.length == branch.body.length + && make_node(AST_BlockStatement, prev, prev).equivalent_to(make_node(AST_BlockStatement, branch, branch))) { + prev.body = []; } - body.push(branch); - fallthrough = false; - } else { - body.push(branch); - fallthrough = true; } + body.push(branch); } - for (; i < len && fallthrough; i++) { - branch = self.body[i]; - exact_match.body = exact_match.body.concat(branch.body); - fallthrough = !aborts(exact_match); - } - while (i < len) extract_declarations_from_unreachable_code(compressor, self.body[i++], decl); + while (i < len) eliminate_branch(self.body[i++], body[body.length - 1]); if (body.length > 0) { body[0].body = decl.concat(body[0].body); } @@ -2721,9 +2735,25 @@ merge(Compressor.prototype, { has_break = true; }); self.walk(tw); - if (!has_break) return make_node(AST_BlockStatement, self, body[0]).optimize(compressor); + if (!has_break) { + body = body[0].body.slice(); + body.unshift(make_node(AST_SimpleStatement, self.expression, { + body: self.expression + })); + return make_node(AST_BlockStatement, self, { + body: body + }).optimize(compressor); + } } return self; + + function eliminate_branch(branch, prev) { + if (prev && !aborts(prev)) { + prev.body = prev.body.concat(branch.body); + } else { + extract_declarations_from_unreachable_code(compressor, branch, decl); + } + } }); OPT(AST_Try, function(self, compressor){ @@ -3036,7 +3066,7 @@ merge(Compressor.prototype, { if (name instanceof AST_SymbolRef && name.name == "console" && name.undeclared()) { - return make_node(AST_Undefined, self).transform(compressor); + return make_node(AST_Undefined, self).optimize(compressor); } } } @@ -3112,7 +3142,7 @@ merge(Compressor.prototype, { } } } - if (is_undefined(self.cdr)) { + if (is_undefined(self.cdr, compressor)) { return make_node(AST_UnaryPrefix, self, { operator : "void", expression : self.car @@ -3151,7 +3181,7 @@ merge(Compressor.prototype, { self.expression = e; return self; } else { - return make_node(AST_Undefined, self).transform(compressor); + return make_node(AST_Undefined, self).optimize(compressor); } } if (compressor.option("booleans") && compressor.in_boolean_context()) { @@ -3175,6 +3205,9 @@ merge(Compressor.prototype, { })).optimize(compressor); } } + if (self.operator == "-" && e instanceof AST_Infinity) { + e = e.transform(compressor); + } if (e instanceof AST_Binary && (self.operator == "+" || self.operator == "-") && (e.operator == "*" || e.operator == "/" || e.operator == "%")) { @@ -3184,8 +3217,7 @@ merge(Compressor.prototype, { } // avoids infinite recursion of numerals if (self.operator != "-" - || !(self.expression instanceof AST_Number - || self.expression instanceof AST_Infinity)) { + || !(e instanceof AST_Number || e instanceof AST_Infinity)) { var ev = self.evaluate(compressor); if (ev !== self) { ev = make_node_from_constant(ev, self).optimize(compressor); @@ -3228,8 +3260,8 @@ merge(Compressor.prototype, { OPT(AST_Binary, function(self, compressor){ function reversible() { - return self.left instanceof AST_Constant - || self.right instanceof AST_Constant + return self.left.is_constant() + || self.right.is_constant() || !self.left.has_side_effects(compressor) && !self.right.has_side_effects(compressor); } @@ -3242,8 +3274,8 @@ merge(Compressor.prototype, { } } if (commutativeOperators(self.operator)) { - if (self.right instanceof AST_Constant - && !(self.left instanceof AST_Constant)) { + if (self.right.is_constant() + && !self.left.is_constant()) { // if right is a constant, whatever side effects the // left side might have could not influence the // result. hence, force switch. @@ -3281,42 +3313,7 @@ merge(Compressor.prototype, { } break; } - if (compressor.option("booleans") && compressor.in_boolean_context()) switch (self.operator) { - case "&&": - var ll = self.left.evaluate(compressor); - var rr = self.right.evaluate(compressor); - if (!ll || !rr) { - compressor.warn("Boolean && always false [{file}:{line},{col}]", self.start); - return make_node(AST_Seq, self, { - car: self.left, - cdr: make_node(AST_False, self) - }).optimize(compressor); - } - if (ll !== self.left && ll) { - return self.right.optimize(compressor); - } - if (rr !== self.right && rr) { - return self.left.optimize(compressor); - } - break; - case "||": - var ll = self.left.evaluate(compressor); - var rr = self.right.evaluate(compressor); - if (ll !== self.left && ll || rr !== self.right && rr) { - compressor.warn("Boolean || always true [{file}:{line},{col}]", self.start); - return make_node(AST_Seq, self, { - car: self.left, - cdr: make_node(AST_True, self) - }).optimize(compressor); - } - if (!ll) { - return self.right.optimize(compressor); - } - if (!rr) { - return self.left.optimize(compressor); - } - break; - case "+": + if (compressor.option("booleans") && self.operator == "+" && compressor.in_boolean_context()) { var ll = self.left.evaluate(compressor); var rr = self.right.evaluate(compressor); if (ll && typeof ll == "string") { @@ -3333,7 +3330,6 @@ merge(Compressor.prototype, { cdr: make_node(AST_True, self) }).optimize(compressor); } - break; } if (compressor.option("comparisons") && self.is_boolean()) { if (!(compressor.parent() instanceof AST_Binary) @@ -3374,24 +3370,48 @@ merge(Compressor.prototype, { if (compressor.option("evaluate")) { switch (self.operator) { case "&&": - if (self.left.is_constant()) { - if (self.left.constant_value(compressor)) { - compressor.warn("Condition left of && always true [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, self.right); - } else { - compressor.warn("Condition left of && always false [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, self.left); + var ll = self.left.evaluate(compressor); + if (!ll) { + compressor.warn("Condition left of && always false [{file}:{line},{col}]", self.start); + return maintain_this_binding(compressor.parent(), self, self.left).optimize(compressor); + } else if (ll !== self.left) { + compressor.warn("Condition left of && always true [{file}:{line},{col}]", self.start); + return maintain_this_binding(compressor.parent(), self, self.right).optimize(compressor); + } + if (compressor.option("booleans") && compressor.in_boolean_context()) { + var rr = self.right.evaluate(compressor); + if (!rr) { + compressor.warn("Boolean && always false [{file}:{line},{col}]", self.start); + return make_node(AST_Seq, self, { + car: self.left, + cdr: make_node(AST_False, self) + }).optimize(compressor); + } else if (rr !== self.right) { + compressor.warn("Dropping side-effect-free && in boolean context [{file}:{line},{col}]", self.start); + return self.left.optimize(compressor); } } break; case "||": - if (self.left.is_constant()) { - if (self.left.constant_value(compressor)) { - compressor.warn("Condition left of || always true [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, self.left); - } else { - compressor.warn("Condition left of || always false [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, self.right); + var ll = self.left.evaluate(compressor); + if (!ll) { + compressor.warn("Condition left of || always false [{file}:{line},{col}]", self.start); + return maintain_this_binding(compressor.parent(), self, self.right).optimize(compressor); + } else if (ll !== self.left) { + compressor.warn("Condition left of || always true [{file}:{line},{col}]", self.start); + return maintain_this_binding(compressor.parent(), self, self.left).optimize(compressor); + } + if (compressor.option("booleans") && compressor.in_boolean_context()) { + var rr = self.right.evaluate(compressor); + if (!rr) { + compressor.warn("Dropping side-effect-free || in boolean context [{file}:{line},{col}]", self.start); + return self.left.optimize(compressor); + } else if (rr !== self.right) { + compressor.warn("Boolean || always true [{file}:{line},{col}]", self.start); + return make_node(AST_Seq, self, { + car: self.left, + cdr: make_node(AST_True, self) + }).optimize(compressor); } } break; @@ -3617,9 +3637,9 @@ merge(Compressor.prototype, { case "undefined": return make_node(AST_Undefined, self).optimize(compressor); case "NaN": - return make_node(AST_NaN, self); + return make_node(AST_NaN, self).optimize(compressor); case "Infinity": - return make_node(AST_Infinity, self); + return make_node(AST_Infinity, self).optimize(compressor); } } if (compressor.option("evaluate") && compressor.option("reduce_vars")) { @@ -3649,19 +3669,48 @@ merge(Compressor.prototype, { OPT(AST_Undefined, function(self, compressor){ if (compressor.option("unsafe")) { - var scope = compressor.find_parent(AST_Scope); - var undef = scope.find_variable("undefined"); + var undef = find_variable(compressor, "undefined"); if (undef) { var ref = make_node(AST_SymbolRef, self, { name : "undefined", - scope : scope, + scope : undef.scope, thedef : undef }); ref.is_undefined = true; return ref; } } - return self; + return make_node(AST_UnaryPrefix, self, { + operator: "void", + expression: make_node(AST_Number, self, { + value: 0 + }) + }); + }); + + OPT(AST_Infinity, function(self, compressor){ + var retain = compressor.option("keep_infinity") && !find_variable(compressor, "Infinity"); + return retain ? self : make_node(AST_Binary, self, { + operator: "/", + left: make_node(AST_Number, self, { + value: 1 + }), + right: make_node(AST_Number, self, { + value: 0 + }) + }); + }); + + 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 ASSIGN_OPS = [ '+', '-', '/', '*', '%', '>>', '<<', '>>>', '|', '^', '&' ]; @@ -3979,7 +4028,7 @@ merge(Compressor.prototype, { OPT(AST_RegExp, literals_in_boolean_context); OPT(AST_Return, function(self, compressor){ - if (self.value && is_undefined(self.value)) { + if (self.value && is_undefined(self.value, compressor)) { self.value = null; } return self; @@ -4000,7 +4049,7 @@ merge(Compressor.prototype, { }); OPT(AST_Yield, function(self, compressor){ - if (!self.is_star && self.expression instanceof AST_Undefined) { + if (self.expression && !self.is_star && is_undefined(self.expression, compressor)) { self.expression = null; } return self; diff --git a/lib/output.js b/lib/output.js index 8c323060..81fb9b89 100644 --- a/lib/output.js +++ b/lib/output.js @@ -53,29 +53,29 @@ function is_some_comments(comment) { function OutputStream(options) { options = defaults(options, { - indent_start : 0, - indent_level : 4, - quote_keys : false, - space_colon : true, ascii_only : false, ascii_identifiers: undefined, - unescape_regexps : false, - inline_script : false, - width : 80, - max_line_len : false, beautify : false, - source_map : null, bracketize : false, - semicolons : true, comments : false, - shebang : true, - preserve_line : false, - screw_ie8 : true, - preamble : null, - quote_style : 0, - keep_quoted_props: false, - shorthand : undefined, ecma : 5, + indent_level : 4, + indent_start : 0, + inline_script : false, + keep_quoted_props: false, + max_line_len : false, + preamble : null, + preserve_line : false, + quote_keys : false, + quote_style : 0, + screw_ie8 : true, + semicolons : true, + shebang : true, + shorthand : undefined, + source_map : null, + space_colon : true, + unescape_regexps : false, + width : 80, wrap_iife : false, }, true); @@ -559,8 +559,8 @@ function OutputStream(options) { })); } - if (comments.length > 0 && output.pos() == 0) { - if (output.option("shebang") && comments[0].type == "comment5") { + if (output.pos() == 0) { + if (comments.length > 0 && output.option("shebang") && comments[0].type == "comment5") { output.print("#!" + comments.shift().value + "\n"); output.indent(); } @@ -640,7 +640,7 @@ function OutputStream(options) { return first_in_statement(output); }); - PARENS([ AST_Unary, AST_Undefined ], function(output){ + PARENS(AST_Unary, function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this || p instanceof AST_Call && p.expression === this @@ -652,15 +652,6 @@ function OutputStream(options) { && this.operator !== "--"; }); - PARENS([ AST_Infinity, AST_NaN ], function(output){ - var p = output.parent(); - return p instanceof AST_PropAccess && p.expression === this - || p instanceof AST_Call && p.expression === this - || p instanceof AST_Unary && p.operator != "+" && p.operator != "-" - || p instanceof AST_Binary && p.right === this - && (p.operator == "/" || p.operator == "%"); - }); - PARENS(AST_Seq, function(output){ var p = output.parent(); return p instanceof AST_Call // (foo, bar)() or foo(1, (2, 3), 4) @@ -1625,24 +1616,7 @@ function OutputStream(options) { DEFPRINT(AST_SymbolDeclaration, function(self, output){ self._do_print(output); }); - DEFPRINT(AST_Undefined, function(self, output){ - output.print("void 0"); - }); DEFPRINT(AST_Hole, noop); - DEFPRINT(AST_Infinity, function(self, output){ - output.print("1"); - output.space(); - output.print("/"); - output.space(); - output.print("0"); - }); - DEFPRINT(AST_NaN, function(self, output){ - output.print("0"); - output.space(); - output.print("/"); - output.space(); - output.print("0"); - }); DEFPRINT(AST_This, function(self, output){ output.print("this"); }); diff --git a/lib/parse.js b/lib/parse.js index 7b7b5aae..9063eead 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -845,14 +845,14 @@ var ATOMIC_START_TOKEN = array_to_hash([ "atom", "num", "string", "regexp", "nam function parse($TEXT, options) { options = defaults(options, { - strict : false, - filename : null, - toplevel : null, - expression : false, - html5_comments : true, bare_returns : false, - shebang : true, cli : false, + expression : false, + filename : null, + html5_comments : true, + shebang : true, + strict : false, + toplevel : null, }); var S = { diff --git a/lib/propmangle.js b/lib/propmangle.js index 5d26fd4c..b5b2caaf 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -79,12 +79,12 @@ function find_builtins() { function mangle_properties(ast, options) { options = defaults(options, { - reserved : null, - cache : null, - only_cache : false, - regex : null, - ignore_quoted : false, - debug : false + cache: null, + debug: false, + ignore_quoted: false, + only_cache: false, + regex: null, + reserved: null, }); var reserved = options.reserved; diff --git a/lib/scope.js b/lib/scope.js index 4a86c5dd..716df6b7 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -100,8 +100,8 @@ SymbolDef.prototype = { AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ options = defaults(options, { + cache: null, screw_ie8: true, - cache: null }); // pass 1: setup scope chaining and handle definitions @@ -490,13 +490,13 @@ AST_Symbol.DEFMETHOD("global", function(){ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { - except : [], eval : false, + except : [], + keep_classnames: false, + keep_fnames : false, + screw_ie8 : true, sort : false, // Ignored. Flag retained for backwards compatibility. toplevel : false, - screw_ie8 : true, - keep_fnames : false, - keep_classnames : false }); }); @@ -681,12 +681,12 @@ var base54 = (function() { AST_Toplevel.DEFMETHOD("scope_warnings", function(options){ options = defaults(options, { - undeclared : false, // this makes a lot of noise - unreferenced : true, assign_to_global : true, + eval : true, func_arguments : true, nested_defuns : true, - eval : true + undeclared : false, // this makes a lot of noise + unreferenced : true, }); var tw = new TreeWalker(function(node){ if (options.undeclared diff --git a/package.json b/package.json index 90172312..d9a97f4a 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.19", + "version": "2.8.21", "engines": { "node": ">=0.8.0" }, diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 54d4264d..e7ea2bb2 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -840,8 +840,8 @@ equality_conditionals_false: { f(0, true, 0), f(1, 2, 3), f(1, null, 3), - f(0/0), - f(0/0, "foo"); + f(NaN), + f(NaN, "foo"); } expect_stdout: true } @@ -888,8 +888,8 @@ equality_conditionals_true: { f(0, true, 0), f(1, 2, 3), f(1, null, 3), - f(0/0), - f(0/0, "foo"); + f(NaN), + f(NaN, "foo"); } expect_stdout: true } diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 2290e1e7..f43ba9c1 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -52,7 +52,7 @@ and: { a = 7; a = false; - a = 0/0; + a = NaN; a = 0; a = void 0; a = null; @@ -67,7 +67,7 @@ and: { a = 6 << condition && -4.5; a = condition && false; - a = console.log("b") && 0/0; + a = console.log("b") && NaN; a = console.log("c") && 0; a = 2 * condition && void 0; a = condition + 3 && null; @@ -149,7 +149,7 @@ or: { a = 6 << condition || -4.5; a = condition || false; - a = console.log("b") || 0/0; + a = console.log("b") || NaN; a = console.log("c") || 0; a = 2 * condition || void 0; a = condition + 3 || null; @@ -302,13 +302,13 @@ pow_with_number_constants: { var m = 3 ** -10; // Result will be 0.000016935087808430286, which is too long } expect: { - var a = 0/0; + var a = NaN; var b = 1; var c = 1; - var d = 0/0; + var d = NaN; var e = 1/0; var f = 0; - var g = 0/0; + var g = NaN; var h = 1/0; var i = -1/0; var j = .125; @@ -627,7 +627,7 @@ unsafe_array: { [1, 2, 3, a][0] + 1, 2, 3, - 0/0, + NaN, "1,21", 5, (void 0)[1] + 1 @@ -896,3 +896,58 @@ issue_1649: { } expect_stdout: "-2"; } + +issue_1760_1: { + options = { + evaluate: true, + } + input: { + !function(a) { + try { + throw 0; + } catch (NaN) { + a = +"foo"; + } + console.log(a); + }(); + } + expect: { + !function(a) { + try { + throw 0; + } catch (NaN) { + a = 0 / 0; + } + console.log(a); + }(); + } + expect_stdout: "NaN" +} + +issue_1760_2: { + options = { + evaluate: true, + keep_infinity: true, + } + input: { + !function(a) { + try { + throw 0; + } catch (Infinity) { + a = 123456789 / 0; + } + console.log(a); + }(); + } + expect: { + !function(a) { + try { + throw 0; + } catch (Infinity) { + a = 1 / 0; + } + console.log(a); + }(); + } + expect_stdout: "Infinity" +} diff --git a/test/compress/expression.js b/test/compress/expression.js index a4be6544..c996845b 100644 --- a/test/compress/expression.js +++ b/test/compress/expression.js @@ -21,13 +21,12 @@ pow_with_number_constants: { var f = 2 ** -Infinity; } expect: { - // TODO: may need parentheses - var a = 5 ** 0/0; + var a = 5 ** NaN; var b = 42 ** +0; var c = 42 ** -0; - var d = 0/0 ** 1; - var e = 2 ** 1/0; - var f = 2 ** -1/0; + var d = NaN ** 1; + var e = 2 ** (1/0); + var f = 2 ** (-1/0); } } diff --git a/test/compress/issue-1105.js b/test/compress/issue-1105.js index f9412165..ea957930 100644 --- a/test/compress/issue-1105.js +++ b/test/compress/issue-1105.js @@ -193,6 +193,7 @@ assorted_Infinity_NaN_undefined_in_with_scope: { cascade: true, side_effects: true, sequences: false, + keep_infinity: false, } input: { var f = console.log; @@ -224,10 +225,73 @@ assorted_Infinity_NaN_undefined_in_with_scope: { }; if (o) { f(void 0, void 0); - f(0/0, 0/0); + f(NaN, NaN); f(1/0, 1/0); f(-1/0, -1/0); - f(0/0, 0/0); + f(NaN, NaN); + } + with (o) { + f(undefined, void 0); + f(NaN, 0/0); + f(Infinity, 1/0); + f(-Infinity, -1/0); + f(9 + undefined, 9 + void 0); + } + } + expect_stdout: true +} + +assorted_Infinity_NaN_undefined_in_with_scope_keep_infinity: { + options = { + unused: true, + evaluate: true, + dead_code: true, + conditionals: true, + comparisons: true, + booleans: true, + hoist_funs: true, + keep_fargs: true, + if_return: true, + join_vars: true, + cascade: true, + side_effects: true, + sequences: false, + keep_infinity: true, + } + input: { + var f = console.log; + var o = { + undefined : 3, + NaN : 4, + Infinity : 5, + }; + if (o) { + f(undefined, void 0); + f(NaN, 0/0); + f(Infinity, 1/0); + f(-Infinity, -(1/0)); + f(2 + 7 + undefined, 2 + 7 + void 0); + } + with (o) { + f(undefined, void 0); + f(NaN, 0/0); + f(Infinity, 1/0); + f(-Infinity, -(1/0)); + f(2 + 7 + undefined, 2 + 7 + void 0); + } + } + expect: { + var f = console.log, o = { + undefined : 3, + NaN : 4, + Infinity : 5 + }; + if (o) { + f(void 0, void 0); + f(NaN, NaN); + f(Infinity, 1/0); + f(-Infinity, -1/0); + f(NaN, NaN); } with (o) { f(undefined, void 0); diff --git a/test/compress/issue-1261.js b/test/compress/issue-1261.js index a872c578..343c175b 100644 --- a/test/compress/issue-1261.js +++ b/test/compress/issue-1261.js @@ -154,12 +154,12 @@ should_warn: { "WARN: Boolean || always true [test/compress/issue-1261.js:129,23]", "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:129,23]", "WARN: Condition always true [test/compress/issue-1261.js:129,23]", - "WARN: Boolean || always true [test/compress/issue-1261.js:130,8]", + "WARN: Condition left of || always true [test/compress/issue-1261.js:130,8]", "WARN: Condition always true [test/compress/issue-1261.js:130,8]", "WARN: Boolean && always false [test/compress/issue-1261.js:131,23]", "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:131,23]", "WARN: Condition always false [test/compress/issue-1261.js:131,23]", - "WARN: Boolean && always false [test/compress/issue-1261.js:132,8]", + "WARN: Condition left of && always false [test/compress/issue-1261.js:132,8]", "WARN: Condition always false [test/compress/issue-1261.js:132,8]", "WARN: + in boolean context always true [test/compress/issue-1261.js:133,23]", "WARN: Dropping __PURE__ call [test/compress/issue-1261.js:133,23]", diff --git a/test/compress/issue-1750.js b/test/compress/issue-1750.js new file mode 100644 index 00000000..d18bc49f --- /dev/null +++ b/test/compress/issue-1750.js @@ -0,0 +1,54 @@ +case_1: { + options = { + dead_code: true, + evaluate: true, + switches: true, + } + input: { + var a = 0, b = 1; + switch (true) { + case a, true: + default: + b = 2; + case true: + } + console.log(a, b); + } + expect: { + var a = 0, b = 1; + switch (true) { + case a, true: + b = 2; + } + console.log(a, b); + } + expect_stdout: "0 2" +} + +case_2: { + options = { + dead_code: true, + evaluate: true, + switches: true, + } + input: { + var a = 0, b = 1; + switch (0) { + default: + b = 2; + case a: + a = 3; + case 0: + } + console.log(a, b); + } + expect: { + var a = 0, b = 1; + switch (0) { + case a: + a = 3; + } + console.log(a, b); + } + expect_stdout: "3 1" +} diff --git a/test/compress/issue-597.js b/test/compress/issue-597.js index 987bcacc..143fcc22 100644 --- a/test/compress/issue-597.js +++ b/test/compress/issue-597.js @@ -6,7 +6,7 @@ NaN_and_Infinity_must_have_parens: { } expect: { (1/0).toString(); - (0/0).toString(); + NaN.toString(); } } @@ -24,6 +24,36 @@ NaN_and_Infinity_should_not_be_replaced_when_they_are_redefined: { } } +NaN_and_Infinity_must_have_parens_evaluate: { + options = { + evaluate: true, + } + input: { + (123456789 / 0).toString(); + (+"foo").toString(); + } + expect: { + (1/0).toString(); + NaN.toString(); + } +} + +NaN_and_Infinity_should_not_be_replaced_when_they_are_redefined_evaluate: { + options = { + evaluate: true, + } + input: { + var Infinity, NaN; + (123456789 / 0).toString(); + (+"foo").toString(); + } + expect: { + var Infinity, NaN; + (1/0).toString(); + (0/0).toString(); + } +} + beautify_off_1: { options = { evaluate: true, diff --git a/test/compress/numbers.js b/test/compress/numbers.js index 946a7f2d..86545fba 100644 --- a/test/compress/numbers.js +++ b/test/compress/numbers.js @@ -186,7 +186,7 @@ unary_binary_parenthesis: { }); } expect: { - var v = [ 0, 1, 0/0, 1/0, null, void 0, true, false, "", "foo", /foo/ ]; + var v = [ 0, 1, NaN, 1/0, null, void 0, true, false, "", "foo", /foo/ ]; v.forEach(function(x) { v.forEach(function(y) { console.log( diff --git a/test/compress/properties.js b/test/compress/properties.js index ef8a8b9b..6eb4c874 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -77,7 +77,7 @@ sub_properties: { a[3.14] = 3; a.if = 4; a["foo bar"] = 5; - a[0/0] = 6; + a[NaN] = 6; a[null] = 7; a[void 0] = 8; } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 87942ab9..cdc4ef20 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1399,6 +1399,8 @@ issue_1670_1: { evaluate: true, dead_code: true, reduce_vars: true, + side_effects: true, + switches: true, unused: true, } input: { @@ -1429,6 +1431,8 @@ issue_1670_2: { dead_code: true, passes: 2, reduce_vars: true, + side_effects: true, + switches: true, unused: true, } input: { @@ -1458,6 +1462,8 @@ issue_1670_3: { evaluate: true, dead_code: true, reduce_vars: true, + side_effects: true, + switches: true, unused: true, } input: { @@ -1488,6 +1494,8 @@ issue_1670_4: { dead_code: true, passes: 2, reduce_vars: true, + side_effects: true, + switches: true, unused: true, } input: { @@ -1516,6 +1524,8 @@ issue_1670_5: { evaluate: true, keep_fargs: false, reduce_vars: true, + side_effects: true, + switches: true, unused: true, } input: { @@ -1544,6 +1554,8 @@ issue_1670_6: { evaluate: true, keep_fargs: false, reduce_vars: true, + side_effects: true, + switches: true, unused: true, } input: { diff --git a/test/compress/sequences.js b/test/compress/sequences.js index f1fa0e87..b3c54635 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -440,3 +440,29 @@ func_def_5: { } expect_stdout: "true" } + +issue_1758: { + options = { + sequences: true, + side_effects: true, + } + input: { + console.log(function(c) { + var undefined = 42; + return function() { + c--; + c--, c.toString(); + return; + }(); + }()); + } + expect: { + console.log(function(c) { + var undefined = 42; + return function() { + return c--, c--, c.toString(), void 0; + }(); + }()); + } + expect_stdout: "undefined" +} diff --git a/test/compress/switch.js b/test/compress/switch.js index 82d725f2..03c1e00a 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -1,5 +1,10 @@ constant_switch_1: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { switch (1+1) { case 1: foo(); break; @@ -13,7 +18,12 @@ constant_switch_1: { } constant_switch_2: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { switch (1) { case 1: foo(); @@ -28,7 +38,12 @@ constant_switch_2: { } constant_switch_3: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { switch (10) { case 1: foo(); @@ -44,7 +59,12 @@ constant_switch_3: { } constant_switch_4: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { switch (2) { case 1: @@ -65,7 +85,12 @@ constant_switch_4: { } constant_switch_5: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { switch (1) { case 1: @@ -94,7 +119,12 @@ constant_switch_5: { } constant_switch_6: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { OUT: { foo(); @@ -123,7 +153,12 @@ constant_switch_6: { } constant_switch_7: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { OUT: { foo(); @@ -161,7 +196,12 @@ constant_switch_7: { } constant_switch_8: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { OUT: switch (1) { case 1: @@ -185,7 +225,12 @@ constant_switch_8: { } constant_switch_9: { - options = { dead_code: true, evaluate: true }; + options = { + dead_code: true, + evaluate: true, + side_effects: true, + switches: true, + } input: { OUT: switch (1) { case 1: @@ -210,7 +255,10 @@ constant_switch_9: { } drop_default_1: { - options = { dead_code: true }; + options = { + dead_code: true, + switches: true, + } input: { switch (foo) { case 'bar': baz(); @@ -225,7 +273,10 @@ drop_default_1: { } drop_default_2: { - options = { dead_code: true }; + options = { + dead_code: true, + switches: true, + } input: { switch (foo) { case 'bar': baz(); break; @@ -241,7 +292,10 @@ drop_default_2: { } keep_default: { - options = { dead_code: true }; + options = { + dead_code: true, + switches: true, + } input: { switch (foo) { case 'bar': baz(); @@ -263,6 +317,8 @@ issue_1663: { options = { dead_code: true, evaluate: true, + side_effects: true, + switches: true, } input: { var a = 100, b = 10; @@ -294,6 +350,7 @@ issue_1663: { drop_case: { options = { dead_code: true, + switches: true, } input: { switch (foo) { @@ -312,6 +369,7 @@ drop_case: { keep_case: { options = { dead_code: true, + switches: true, } input: { switch (foo) { @@ -332,6 +390,7 @@ issue_376: { options = { dead_code: true, evaluate: true, + switches: true, } input: { switch (true) { @@ -354,6 +413,7 @@ issue_376: { issue_441_1: { options = { dead_code: true, + switches: true, } input: { switch (foo) { @@ -381,6 +441,7 @@ issue_441_1: { issue_441_2: { options = { dead_code: true, + switches: true, } input: { switch (foo) { @@ -414,6 +475,8 @@ issue_1674: { options = { dead_code: true, evaluate: true, + side_effects: true, + switches: true, } input: { switch (0) { @@ -435,6 +498,7 @@ issue_1679: { options = { dead_code: true, evaluate: true, + switches: true, } input: { var a = 100, b = 10; @@ -482,6 +546,7 @@ issue_1680_1: { options = { dead_code: true, evaluate: true, + switches: true, } input: { function f(x) { @@ -522,6 +587,7 @@ issue_1680_1: { issue_1680_2: { options = { dead_code: true, + switches: true, } input: { var a = 100, b = 10; @@ -557,6 +623,7 @@ issue_1680_2: { issue_1690_1: { options = { dead_code: true, + switches: true, } input: { switch (console.log("PASS")) {} @@ -570,6 +637,7 @@ issue_1690_1: { issue_1690_2: { options = { dead_code: false, + switches: true, } input: { switch (console.log("PASS")) {} @@ -585,6 +653,7 @@ if_switch_typeof: { conditionals: true, dead_code: true, side_effects: true, + switches: true, } input: { if (a) switch(typeof b) {} @@ -597,6 +666,7 @@ if_switch_typeof: { issue_1698: { options = { side_effects: true, + switches: true, } input: { var a = 1; @@ -618,6 +688,7 @@ issue_1698: { issue_1705_1: { options = { dead_code: true, + switches: true, } input: { var a = 0; @@ -646,6 +717,7 @@ issue_1705_2: { reduce_vars: true, sequences: true, side_effects: true, + switches: true, toplevel: true, unused: true, } @@ -666,6 +738,7 @@ issue_1705_2: { issue_1705_3: { options = { dead_code: true, + switches: true, } input: { switch (a) { @@ -721,3 +794,25 @@ beautify: { "}", ] } + +issue_1758: { + options = { + dead_code: true, + switches: true, + } + input: { + var a = 1, b = 2; + switch (a--) { + default: + b++; + } + console.log(a, b); + } + expect: { + var a = 1, b = 2; + a--; + b++; + console.log(a, b); + } + expect_stdout: "0 3" +} diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js index 9474e732..ec17aa8c 100644 --- a/test/mocha/comment-filter.js +++ b/test/mocha/comment-filter.js @@ -79,5 +79,13 @@ describe("comment filters", function() { output: { preamble: "/* Build */" } }).code; assert.strictEqual(code, "#!/usr/bin/node\n/* Build */\nvar x=10;"); - }) + }); + + it("Should handle preamble without shebang correctly", function() { + var code = UglifyJS.minify("var x = 10;", { + fromString: true, + output: { preamble: "/* Build */" } + }).code; + assert.strictEqual(code, "/* Build */\nvar x=10;"); + }); }); diff --git a/test/run-tests.js b/test/run-tests.js index 4870873d..f3c62e7a 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -4,22 +4,11 @@ var U = require("../tools/node"); var path = require("path"); var fs = require("fs"); var assert = require("assert"); -var vm = require("vm"); +var sandbox = require("./sandbox"); var tests_dir = path.dirname(module.filename); var failures = 0; var failed_files = {}; -var same_stdout = ~process.version.lastIndexOf("v0.12.", 0) ? function(expected, actual) { - if (typeof expected != typeof actual) return false; - if (typeof expected != "string") { - if (expected.name != actual.name) return false; - expected = expected.message.slice(expected.message.lastIndexOf("\n") + 1); - actual = actual.message.slice(actual.message.lastIndexOf("\n") + 1); - } - return expected == actual; -} : function(expected, actual) { - return typeof expected == typeof actual && expected.toString() == actual.toString(); -}; run_compress_tests(); if (failures) { @@ -182,11 +171,11 @@ function run_compress_tests() { } } if (test.expect_stdout) { - var stdout = run_code(input_code); + var stdout = sandbox.run_code(input_code); if (test.expect_stdout === true) { test.expect_stdout = stdout; } - if (!same_stdout(test.expect_stdout, stdout)) { + if (!sandbox.same_stdout(test.expect_stdout, stdout)) { log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { input: input_formatted, expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", @@ -197,8 +186,8 @@ function run_compress_tests() { failures++; failed_files[file] = 1; } else { - stdout = run_code(output); - if (!same_stdout(test.expect_stdout, stdout)) { + stdout = sandbox.run_code(output); + if (!sandbox.same_stdout(test.expect_stdout, stdout)) { log("!!! failed\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { input: input_formatted, expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", @@ -330,19 +319,3 @@ function evaluate(code) { code = make_code(code, { beautify: true }); return new Function("return(" + code + ")")(); } - -function run_code(code) { - var stdout = ""; - var original_write = process.stdout.write; - process.stdout.write = function(chunk) { - stdout += chunk; - }; - try { - new vm.Script(code).runInNewContext({ console: console }, { timeout: 5000 }); - return stdout; - } catch (ex) { - return ex; - } finally { - process.stdout.write = original_write; - } -} diff --git a/test/sandbox.js b/test/sandbox.js new file mode 100644 index 00000000..504e5e25 --- /dev/null +++ b/test/sandbox.js @@ -0,0 +1,50 @@ +var vm = require("vm"); + +var FUNC_TOSTRING = [ + "Function.prototype.toString = Function.prototype.valueOf = function() {", + " var ids = [];", + " return function() {", + " var i = ids.indexOf(this);", + " if (i < 0) {", + " i = ids.length;", + " ids.push(this);", + " }", + ' return "[Function: __func_" + i + "__]";', + " }", + "}();", + "" +].join("\n"); +exports.run_code = function(code) { + var stdout = ""; + var original_write = process.stdout.write; + process.stdout.write = function(chunk) { + stdout += chunk; + }; + try { + new vm.Script(FUNC_TOSTRING + code).runInNewContext({ + console: { + log: function() { + return console.log.apply(console, [].map.call(arguments, function(arg) { + return typeof arg == "function" ? arg.toString() : arg; + })); + } + } + }, { timeout: 30000 }); + return stdout; + } catch (ex) { + return ex; + } finally { + process.stdout.write = original_write; + } +}; +exports.same_stdout = ~process.version.lastIndexOf("v0.12.", 0) ? function(expected, actual) { + if (typeof expected != typeof actual) return false; + if (typeof expected != "string") { + if (expected.name != actual.name) return false; + expected = expected.message.slice(expected.message.lastIndexOf("\n") + 1); + actual = actual.message.slice(actual.message.lastIndexOf("\n") + 1); + } + return expected == actual; +} : function(expected, actual) { + return typeof expected == typeof actual && expected.toString() == actual.toString(); +}; diff --git a/test/ufuzz.js b/test/ufuzz.js index c1ac8f4c..7aab6715 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -2,476 +2,759 @@ // derived from https://github.com/qfox/uglyfuzzer by Peter van der Zee "use strict"; +// check both cli and file modes of nodejs (!). See #1695 for details. and the various settings of uglify. +// bin/uglifyjs s.js -c && bin/uglifyjs s.js -c passes=3 && bin/uglifyjs s.js -c passes=3 -m +// cat s.js | node && node s.js && bin/uglifyjs s.js -c | node && bin/uglifyjs s.js -c passes=3 | node && bin/uglifyjs s.js -c passes=3 -m | node + // workaround for tty output truncation upon process.exit() [process.stdout, process.stderr].forEach(function(stream){ if (stream._handle && stream._handle.setBlocking) stream._handle.setBlocking(true); }); -var vm = require("vm"); -var minify = require("..").minify; +var UglifyJS = require(".."); +var randomBytes = require("crypto").randomBytes; +var sandbox = require("./sandbox"); -var MAX_GENERATED_FUNCTIONS_PER_RUN = 1; -var MAX_GENERATION_RECURSION_DEPTH = 15; +var MAX_GENERATED_TOPLEVELS_PER_RUN = 1; +var MAX_GENERATION_RECURSION_DEPTH = 12; var INTERVAL_COUNT = 100; +var STMT_BLOCK = 0; +var STMT_IF_ELSE = 1; +var STMT_DO_WHILE = 2; +var STMT_WHILE = 3; +var STMT_FOR_LOOP = 4; +var STMT_SEMI = 5; +var STMT_EXPR = 6; +var STMT_SWITCH = 7; +var STMT_VAR = 8; +var STMT_RETURN_ETC = 9; +var STMT_FUNC_EXPR = 10; +var STMT_TRY = 11; +var STMT_C = 12; +var STMTS_TO_USE = [ + STMT_BLOCK, + STMT_IF_ELSE, + STMT_DO_WHILE, + STMT_WHILE, + STMT_FOR_LOOP, + STMT_SEMI, + STMT_EXPR, + STMT_SWITCH, + STMT_VAR, + STMT_RETURN_ETC, + STMT_FUNC_EXPR, + STMT_TRY, + STMT_C, +]; +var STMT_ARG_TO_ID = { + block: STMT_BLOCK, + ifelse: STMT_IF_ELSE, + dowhile: STMT_DO_WHILE, + while: STMT_WHILE, + forloop: STMT_FOR_LOOP, + semi: STMT_SEMI, + expr: STMT_EXPR, + switch: STMT_SWITCH, + var: STMT_VAR, + stop: STMT_RETURN_ETC, + funcexpr: STMT_FUNC_EXPR, + try: STMT_TRY, + c: STMT_C, +}; + +var STMT_FIRST_LEVEL_OVERRIDE = -1; +var STMT_SECOND_LEVEL_OVERRIDE = -1; +var STMT_COUNT_FROM_GLOBAL = true; // count statement depth from nearest function scope or just global scope? + +var num_iterations = +process.argv[2] || 1/0; +var verbose = false; // log every generated test +var verbose_interval = false; // log every 100 generated tests +for (var i = 2; i < process.argv.length; ++i) { + switch (process.argv[i]) { + case '-v': + verbose = true; + break; + case '-V': + verbose_interval = 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'); + break; + case '-r': + MAX_GENERATION_RECURSION_DEPTH = +process.argv[++i]; + if (!MAX_GENERATION_RECURSION_DEPTH) throw new Error('Recursion depth must be at least 1'); + break; + case '-s1': + var name = process.argv[++i]; + STMT_FIRST_LEVEL_OVERRIDE = STMT_ARG_TO_ID[name]; + if (!(STMT_FIRST_LEVEL_OVERRIDE >= 0)) throw new Error('Unknown statement name; use -? to get a list'); + break; + case '-s2': + var name = process.argv[++i]; + STMT_SECOND_LEVEL_OVERRIDE = STMT_ARG_TO_ID[name]; + if (!(STMT_SECOND_LEVEL_OVERRIDE >= 0)) throw new Error('Unknown statement name; use -? to get a list'); + break; + case '--stmt-depth-from-func': + STMT_COUNT_FROM_GLOBAL = false; + break; + case '--only-stmt': + STMTS_TO_USE = process.argv[++i].split(',').map(function(name){ return STMT_ARG_TO_ID[name]; }); + break; + case '--without-stmt': + // meh. it runs once it's fine. + process.argv[++i].split(',').forEach(function(name){ + var omit = STMT_ARG_TO_ID[name]; + STMTS_TO_USE = STMTS_TO_USE.filter(function(id){ return id !== omit; }) + }); + break; + case '--help': + case '-h': + case '-?': + console.log('** UglifyJS fuzzer help **'); + console.log('Valid options (optional):'); + 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('-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)'); + console.log('-s2 : force the second level statement to be this one (see list below)'); + console.log('--stmt-depth-from-func: reset statement depth counter at each function, counts from global otherwise'); + console.log('--only-stmt : a comma delimited white list of statements that may be generated'); + console.log('--without-stmt : a comma delimited black list of statements never to generate'); + console.log('List of accepted statement names: ' + Object.keys(STMT_ARG_TO_ID)); + console.log('** UglifyJS fuzzer exiting **'); + return 0; + default: + // first arg may be a number. + if (i > 2 || !parseInt(process.argv[i], 10)) throw new Error('Unknown argument[' + process.argv[i] + ']; see -h for help'); + } +} + var VALUES = [ - 'true', - 'false', - '22', - '0', - '-0', // 0/-0 !== 0 - '23..toString()', - '24 .toString()', - '25. ', - '0x26.toString()', - '(-1)', - 'NaN', - 'undefined', - 'Infinity', - 'null', - '[]', - '[,0][1]', // an array with elisions... but this is always false - '([,0].length === 2)', // an array with elisions... this is always true - '({})', // wrapped the object causes too many syntax errors in statements - '"foo"', - '"bar"' ]; + 'true', + 'false', + ' /[a2][^e]+$/ ', + '(-1)', + '(-2)', + '(-3)', + '(-4)', + '(-5)', + '0', + '1', + '2', + '3', + '4', + '5', + '22', + '-0', // 0/-0 !== 0 + '23..toString()', + '24 .toString()', + '25. ', + '0x26.toString()', + 'NaN', + 'undefined', + 'Infinity', + 'null', + '[]', + '[,0][1]', // an array with elisions... but this is always false + '([,0].length === 2)', // an array with elisions... this is always true + '({})', // wrapped the object causes too many syntax errors in statements + '"foo"', + '"bar"', + '"undefined"', + '"object"', + '"number"', + '"function"', +]; var BINARY_OPS_NO_COMMA = [ - ' + ', // spaces needed to disambiguate with ++ cases (could otherwise cause syntax errors) - ' - ', - '/', - '*', - '&', - '|', - '^', - '<<', - '>>', - '>>>', - '%', - '&&', - '||', - '^' ]; + ' + ', // spaces needed to disambiguate with ++ cases (could otherwise cause syntax errors) + ' - ', + '/', + '*', + '&', + '|', + '^', + '<', + '<=', + '>', + '>=', + '==', + '===', + '!=', + '!==', + '<<', + '>>', + '>>>', + '%', + '&&', + '||', + '^' ]; var BINARY_OPS = [','].concat(BINARY_OPS_NO_COMMA); var ASSIGNMENTS = [ - '=', - '=', - '=', - '=', - '=', - '=', + '=', + '=', + '=', + '=', + '=', + '=', - '==', - '!=', - '===', - '!==', - '+=', - '-=', - '*=', - '/=', - '&=', - '|=', - '^=', - '<<=', - '>>=', - '>>>=', - '%=' ]; + '==', + '!=', + '===', + '!==', + '+=', + '-=', + '*=', + '/=', + '&=', + '|=', + '^=', + '<<=', + '>>=', + '>>>=', + '%=' ]; var UNARY_OPS = [ - '--', - '++', - '~', - '!', - 'void ', - 'delete ', // should be safe, even `delete foo` and `delete f()` shouldn't crash - ' - ', - ' + ' ]; + '--', + '++', + '~', + '!', + 'void ', + 'delete ', // should be safe, even `delete foo` and `delete f()` shouldn't crash + ' - ', + ' + ' ]; var NO_COMMA = true; +var COMMA_OK = false; var MAYBE = true; -var NESTED = true; +var MANDATORY = false; var CAN_THROW = true; var CANNOT_THROW = false; var CAN_BREAK = true; +var CANNOT_BREAK = false; var CAN_CONTINUE = true; +var CANNOT_CONTINUE = false; +var CAN_RETURN = false; +var CANNOT_RETURN = true; +var NOT_GLOBAL = true; +var IN_GLOBAL = true; +var ANY_TYPE = false; +var NO_DECL = true; +var DONT_STORE = true; var VAR_NAMES = [ - 'foo', - 'bar', - 'a', - 'b', - '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 - 'let' ]; // maybe omit this, it's more a parser problem than minifier + 'foo', + 'bar', + 'a', + '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 + 'parseInt', + 'parseFloat', + 'isNaN', + 'isFinite', + 'decodeURI', + 'decodeURIComponent', + 'encodeURI', + 'encodeURIComponent', + 'Object']; +var INITIAL_NAMES_LEN = VAR_NAMES.length; var TYPEOF_OUTCOMES = [ - 'undefined', - 'string', - 'number', - 'object', - 'boolean', - 'special', - 'unknown', - 'symbol', - 'crap' ]; - -var FUNC_TOSTRING = [ - "Function.prototype.toString = function() {", - " var ids = [];", - " return function() {", - " var i = ids.indexOf(this);", - " if (i < 0) {", - " i = ids.length;", - " ids.push(this);", - " }", - ' return "[Function: __func_" + i + "__]";', - " }", - "}();", - "" -].join("\n"); - -function run_code(code) { - var stdout = ""; - var original_write = process.stdout.write; - process.stdout.write = function(chunk) { - stdout += chunk; - }; - try { - new vm.Script(FUNC_TOSTRING + code).runInNewContext({ - console: { - log: function() { - return console.log.apply(console, [].map.call(arguments, function(arg) { - return typeof arg == "function" ? "[Function]" : arg; - })); - } - } - }, { timeout: 5000 }); - return stdout; - } catch (ex) { - return ex; - } finally { - process.stdout.write = original_write; - } -} - -function rng(max) { - return Math.floor(max * Math.random()); -} - -function createFunctionDecls(n, recurmax, nested) { - if (--recurmax < 0) { return ';'; } - var s = ''; - while (n-- > 0) { - s += createFunctionDecl(recurmax, nested) + '\n'; - } - return s; -} - -var funcs = 0; -function createFunctionDecl(recurmax, nested) { - if (--recurmax < 0) { return ';'; } - var func = funcs++; - var name = rng(5) > 0 ? 'f' + func : createVarName(); - if (name === 'a' || name === 'b') name = 'f' + func; // quick hack to prevent assignment to func names of being called - if (!nested && name === 'undefined' || name === 'NaN' || name === 'Infinity') name = 'f' + func; // cant redefine these in global space - var s = ''; - if (rng(5) === 1) { - // functions with functions. lower the recursion to prevent a mess. - s = 'function ' + name + '(){' + createFunctionDecls(rng(5) + 1, Math.ceil(recurmax / 2), NESTED) + '}\n'; - } else { - // functions with statements - s = 'function ' + name + '(){' + createStatements(3, recurmax) + '}\n'; - } - - if (nested) s = '!' + nested; // avoid "function statements" (decl inside statements) - else s += name + '();' - - return s; -} - -function createStatements(n, recurmax, canThrow, canBreak, canContinue) { - if (--recurmax < 0) { return ';'; } - var s = ''; - while (--n > 0) { - s += createStatement(recurmax, canThrow, canBreak, canContinue); - } - return s; -} + 'function', + 'undefined', + 'string', + 'number', + 'object', + 'boolean', + 'special', + 'unknown', + 'symbol', + 'crap' ]; var loops = 0; -function createStatement(recurmax, canThrow, canBreak, canContinue) { - var loop = ++loops; - if (--recurmax < 0) { return ';'; } - switch (rng(16)) { - case 0: - return '{' + createStatements(rng(5) + 1, recurmax, canThrow, canBreak, canContinue) + '}'; - case 1: - return 'if (' + createExpression(recurmax) + ')' + createStatement(recurmax, canThrow, canBreak, canContinue) + (rng(2) === 1 ? ' else ' + createStatement(recurmax, canThrow, canBreak, canContinue) : ''); - case 2: - return '{var brake' + loop + ' = 5; do {' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE) + '} while ((' + createExpression(recurmax) + ') && --brake' + loop + ' > 0);}'; - case 3: - return '{var brake' + loop + ' = 5; while ((' + createExpression(recurmax) + ') && --brake' + loop + ' > 0)' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE) + '}'; - case 4: - return 'for (var brake' + loop + ' = 5; (' + createExpression(recurmax) + ') && brake' + loop + ' > 0; --brake' + loop + ')' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE); - case 5: - return ';'; - case 6: - return createExpression(recurmax) + ';'; - case 7: - // note: case args are actual expressions - // note: default does not _need_ to be last - return 'switch (' + createExpression(recurmax) + ') { ' + createSwitchParts(recurmax, 4) + '}'; - case 8: - return 'var ' + createVarName() + ';'; - case 9: - // initializer can only have one expression - return 'var ' + createVarName() + ' = ' + createExpression(recurmax, NO_COMMA) + ';'; - case 10: - // initializer can only have one expression - return 'var ' + createVarName() + ' = ' + createExpression(recurmax, NO_COMMA) + ', ' + createVarName() + ' = ' + createExpression(recurmax, NO_COMMA) + ';'; - case 11: - if (canBreak && rng(5) === 0) return 'break;'; - if (canContinue && rng(5) === 0) return 'continue;'; - return 'return;'; - case 12: - // must wrap in curlies to prevent orphaned `else` statement - if (canThrow && rng(5) === 0) return '{ throw ' + createExpression(recurmax) + '}'; - return '{ return ' + createExpression(recurmax) + '}'; - case 13: - // 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 - if (canThrow && rng(5) === 0) return '{ throw\n' + createExpression(recurmax) + '}'; - return '{ return\n' + createExpression(recurmax) + '}'; - case 14: - // "In non-strict mode code, functions can only be declared at top level, inside a block, or ..." - // (dont both with func decls in `if`; it's only a parser thing because you cant call them without a block) - return '{' + createFunctionDecl(recurmax, NESTED) + '}'; - case 15: - return ';'; - // catch var could cause some problems - // note: the "blocks" are syntactically mandatory for try/catch/finally - var s = 'try {' + createStatement(recurmax, CAN_THROW, canBreak, canContinue) + ' }'; - var n = rng(3); // 0=only catch, 1=only finally, 2=catch+finally - if (n !== 1) s += ' catch (' + createVarName() + ') { ' + createStatements(3, recurmax, canBreak, canContinue) + ' }'; - if (n !== 0) s += ' finally { ' + createStatements(3, recurmax, canBreak, canContinue) + ' }'; - return s; - } +var funcs = 0; + +function rng(max) { + var r = randomBytes(2).readUInt16LE(0) / 65536; + return Math.floor(max * r); } -function createSwitchParts(recurmax, n) { - var hadDefault = false; - var s = ''; - while (n-- > 0) { - hadDefault = n > 0; - if (hadDefault || rng(4) > 0) { - s += '' + - 'case ' + createExpression(recurmax) + ':\n' + - createStatements(rng(3) + 1, recurmax, CANNOT_THROW, CAN_BREAK) + - '\n' + - (rng(10) > 0 ? ' break;' : '/* fall-through */') + - '\n'; - } else { - hadDefault = true; - s += '' + - 'default:\n' + - createStatements(rng(3) + 1, recurmax, CANNOT_THROW, CAN_BREAK) + - '\n'; +function createTopLevelCode() { + if (rng(2) === 0) return createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0); + return createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, IN_GLOBAL, ANY_TYPE, CANNOT_THROW, 0); +} + +function createFunctions(n, recurmax, inGlobal, noDecl, canThrow, stmtDepth) { + if (--recurmax < 0) { return ';'; } + var s = ''; + while (n-- > 0) { + s += createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) + '\n'; } - } - return s; + return s; } -function createExpression(recurmax, noComma) { - if (--recurmax < 0) { - return createValue(); // note: should return a simple non-recursing expression value! - } - switch (rng(12)) { - case 0: - return '(' + createUnaryOp() + (rng(2) === 1 ? 'a' : 'b') + ')'; - case 1: - return '(a' + (rng(2) == 1 ? '++' : '--') + ')'; - case 2: - return '(b ' + createAssignment() + ' a)'; - case 3: - return '(' + rng(2) + ' === 1 ? a : b)'; - case 4: - return createExpression(recurmax, noComma) + createBinaryOp(noComma) + createExpression(recurmax, noComma); - case 5: - return createValue(); - case 6: - return '(' + createExpression(recurmax) + ')'; - case 7: - return createExpression(recurmax, noComma) + '?(' + createExpression(recurmax) + '):(' + createExpression(recurmax) + ')'; - case 8: - switch(rng(4)) { - case 0: - return '(function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '})()'; - case 1: - return '+function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; - case 2: - return '!function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; - case 3: - return 'void function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; - default: - return 'void function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; - } - case 9: - return createTypeofExpr(recurmax); - 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) + '\n' : '') + - 'return Math;\n' + - '}'; - case 11: - // more like a parser test but perhaps comment nodes mess up the analysis? - switch (rng(5)) { - case 0: - return '(a/* ignore */++)'; - case 1: - return '(b/* ignore */--)'; - case 2: - return '(++/* ignore */a)'; - case 3: - 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))'; - default: - return '(--/* ignore */b)'; - } - } +function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { + if (--recurmax < 0) { return ';'; } + if (!STMT_COUNT_FROM_GLOBAL) stmtDepth = 0; + var func = funcs++; + var namesLenBefore = VAR_NAMES.length; + var name = (inGlobal || rng(5) > 0) ? 'f' + func : createVarName(MANDATORY, noDecl); + if (name === 'a' || name === 'b' || name === 'c') name = 'f' + func; // quick hack to prevent assignment to func names of being called + 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'; + } else { + // functions with statements + s = 'function ' + name + '(' + createVarName(MANDATORY) + '){' + 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) + ')'; + // avoid "function statements" (decl inside statements) + else if (inGlobal || rng(10) > 0) s += name + '();' + + + return s; } -function createTypeofExpr(recurmax) { - if (--recurmax < 0) { - return 'typeof undefined === "undefined"'; - } +function createStatements(n, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { + if (--recurmax < 0) { return ';'; } + var s = ''; + while (--n > 0) { + s += createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '\n'; + } + return s; +} - switch (rng(5)) { - case 0: - return '(typeof ' + createVarName() + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; - case 1: - return '(typeof ' + createVarName() + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; - case 2: - return '(typeof ' + createVarName() + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; - case 3: - return '(typeof ' + createVarName() + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; - case 4: - return '(typeof ' + createVarName() + ')'; - } +function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { + ++stmtDepth; + var loop = ++loops; + if (--recurmax < 0) { + return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';'; + } + + // allow to forcefully generate certain structures at first or second recursion level + var target = 0; + if (stmtDepth === 1 && STMT_FIRST_LEVEL_OVERRIDE >= 0) target = STMT_FIRST_LEVEL_OVERRIDE; + else if (stmtDepth === 2 && STMT_SECOND_LEVEL_OVERRIDE >= 0) target = STMT_SECOND_LEVEL_OVERRIDE; + else target = STMTS_TO_USE[rng(STMTS_TO_USE.length)]; + + switch (target) { + case STMT_BLOCK: + return '{' + createStatements(rng(5) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '}'; + case STMT_IF_ELSE: + return 'if (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + (rng(2) === 1 ? ' else ' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) : ''); + case STMT_DO_WHILE: + return '{var brake' + loop + ' = 5; do {' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE, cannotReturn, stmtDepth) + '} while ((' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && --brake' + loop + ' > 0);}'; + case STMT_WHILE: + return '{var brake' + loop + ' = 5; while ((' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && --brake' + loop + ' > 0)' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE, cannotReturn, stmtDepth) + '}'; + case STMT_FOR_LOOP: + return 'for (var brake' + loop + ' = 5; (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && brake' + loop + ' > 0; --brake' + loop + ')' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE, cannotReturn, stmtDepth); + case STMT_SEMI: + return ';'; + case STMT_EXPR: + return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';'; + case STMT_SWITCH: + // note: case args are actual expressions + // note: default does not _need_ to be last + return 'switch (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') { ' + createSwitchParts(recurmax, 4, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '}'; + case STMT_VAR: + switch (rng(3)) { + case 0: + var name = createVarName(MANDATORY); + if (name === 'c') name = 'a'; + return 'var ' + name + ';'; + case 1: + // initializer can only have one expression + var name = createVarName(MANDATORY); + if (name === 'c') name = 'b'; + return 'var ' + name + ' = ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; + default: + // initializer can only have one expression + var n1 = createVarName(MANDATORY); + if (n1 === 'c') n1 = 'b'; + var n2 = createVarName(MANDATORY); + if (n2 === 'c') n2 = 'b'; + return 'var ' + n1 + ' = ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ', ' + n2 + ' = ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; + } + case STMT_RETURN_ETC: + switch (rng(3)) { + case 1: + 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: + // 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) + '}'; + } + case STMT_FUNC_EXPR: + // "In non-strict mode code, functions can only be declared at top level, inside a block, or ..." + // (dont both with func decls in `if`; it's only a parser thing because you cant call them without a block) + return '{' + createFunction(recurmax, NOT_GLOBAL, NO_DECL, canThrow, stmtDepth) + '}'; + case STMT_TRY: + // catch var could cause some problems + // note: the "blocks" are syntactically mandatory for try/catch/finally + var n = rng(3); // 0=only catch, 1=only finally, 2=catch+finally + var s = 'try {' + createStatement(recurmax, n === 1 ? CANNOT_THROW : CAN_THROW, canBreak, canContinue, cannotReturn, stmtDepth) + ' }'; + if (n !== 1) { + // the catch var should only be accessible in the catch clause... + // we have to do go through some trouble here to prevent leaking it + var nameLenBefore = VAR_NAMES.length; + var catchName = createVarName(MANDATORY); + var freshCatchName = VAR_NAMES.length !== nameLenBefore; + s += ' catch (' + catchName + ') { ' + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + ' }'; + if (freshCatchName) VAR_NAMES.splice(nameLenBefore, 1); // remove catch name + } + if (n !== 0) s += ' finally { ' + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + ' }'; + return s; + case STMT_C: + return 'c = c + 1;'; + default: + throw 'no'; + } +} + +function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { + var hadDefault = false; + var s = ''; + while (n-- > 0) { + //hadDefault = n > 0; // disables weird `default` clause positioning (use when handling destabilizes) + if (hadDefault || rng(5) > 0) { + s += '' + + 'case ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':\n' + + createStatements(rng(3) + 1, recurmax, canThrow, CAN_BREAK, canContinue, cannotReturn, stmtDepth) + + '\n' + + (rng(10) > 0 ? ' break;' : '/* fall-through */') + + '\n'; + } else { + hadDefault = true; + s += '' + + 'default:\n' + + createStatements(rng(3) + 1, recurmax, canThrow, CAN_BREAK, canContinue, cannotReturn, stmtDepth) + + '\n'; + } + } + return s; +} + +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! + } + // 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); +} +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: + // 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 rng(2) + ' === 1 ? a : b'; + case 4: + return createNestedBinaryExpr(recurmax, noComma) + createBinaryOp(noComma) + createExpression(recurmax, noComma, stmtDepth, canThrow); + case 5: + return createValue(); + case 6: + return '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; + case 7: + return createExpression(recurmax, noComma, stmtDepth, canThrow) + '?' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':' + createExpression(recurmax, noComma, stmtDepth, canThrow); + case 8: + 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'; + var s = ''; + switch(rng(4)) { + case 0: + s = '(function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '})()'; + break; + case 1: + s = '+function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}()'; + break; + case 2: + s = '!function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}()'; + break; + default: + s = 'void function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}()'; + break; + } + VAR_NAMES.length = nameLenBefore; + return s; + case 9: + 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: + // 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. + switch (rng(6)) { + case 0: + return 'a/* ignore */++'; + case 1: + return 'b/* ignore */--'; + case 2: + return '(++/* ignore */a)'; + case 3: + 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))'; + 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)'; + } + case 12: + return createNestedBinaryExpr(recurmax, noComma); + case 13: + return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || a || 3).toString() "; + case 14: + return " /[abc4]/.test(((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || b || 5).toString()) "; + } +} + +function createNestedBinaryExpr(recurmax, noComma) { + recurmax = 3; // note that this generates 2^recurmax expression parts... make sure to cap it + return _createSimpleBinaryExpr(recurmax, noComma); +} +function _createSimpleBinaryExpr(recurmax, noComma) { + // 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 + ')'; + } + return s; +} + +function createTypeofExpr(recurmax, stmtDepth, canThrow) { + switch (rng(8)) { + case 0: + 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)] + '"'; + case 2: + 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)] + '"'; + case 4: + return 'typeof ' + createVarName(MANDATORY, DONT_STORE); + default: + return '(typeof ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; + } } function createValue() { - return VALUES[rng(VALUES.length)]; + return VALUES[rng(VALUES.length)]; } function createBinaryOp(noComma) { - if (noComma) return BINARY_OPS_NO_COMMA[rng(BINARY_OPS_NO_COMMA.length)]; - return BINARY_OPS[rng(BINARY_OPS.length)]; + if (noComma) return BINARY_OPS_NO_COMMA[rng(BINARY_OPS_NO_COMMA.length)]; + return BINARY_OPS[rng(BINARY_OPS.length)]; } function createAssignment() { - return ASSIGNMENTS[rng(ASSIGNMENTS.length)]; + return ASSIGNMENTS[rng(ASSIGNMENTS.length)]; } function createUnaryOp() { - return UNARY_OPS[rng(UNARY_OPS.length)]; + return UNARY_OPS[rng(UNARY_OPS.length)]; } -function createVarName(maybe) { - if (!maybe || rng(2) === 1) { - return VAR_NAMES[rng(VAR_NAMES.length)] + (rng(5) > 0 ? ++loops : ''); - } - return ''; +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); + return name; + } + return ''; } -function log(ok) { - console.log("//============================================================="); - if (!ok) console.log("// !!!!!! Failed..."); - console.log("// original code"); - console.log("//"); - console.log(original_code); - console.log(); - console.log(); - console.log("//-------------------------------------------------------------"); - console.log("// original code (beautify'd)"); - console.log("//"); - console.log(beautify_code); - console.log(); - console.log(); - console.log("//-------------------------------------------------------------"); - console.log("// uglified code"); - console.log("//"); - console.log(uglify_code); - console.log(); - console.log(); - console.log("original result:"); - console.log(original_result); - console.log("beautified result:"); - console.log(beautify_result); - console.log("uglified result:"); - console.log(uglify_result); - if (!ok) console.log("!!!!!! Failed..."); -} - -var num_iterations = +process.argv[2] || 1/0; -var verbose = process.argv[3] === 'v' || process.argv[2] === 'v'; -var verbose_interval = process.argv[3] === 'V' || process.argv[2] === 'V'; -for (var round = 0; round < num_iterations; round++) { - var parse_error = false; - process.stdout.write(round + " of " + num_iterations + "\r"); - var original_code = [ - "var a = 100, b = 10;", - createFunctionDecls(rng(MAX_GENERATED_FUNCTIONS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH), - "console.log(a, b);" - ].join("\n"); - var original_result = run_code(original_code); - +function try_beautify(code, result) { try { - var beautify_code = minify(original_code, { + var beautified = UglifyJS.minify(code, { fromString: true, - mangle: false, compress: false, + mangle: false, output: { beautify: true, bracketize: true, }, }).code; - } catch(e) { - parse_error = 1; + if (sandbox.same_stdout(sandbox.run_code(beautified), result)) { + console.log("// (beautified)"); + console.log(beautified); + return; + } + } catch (e) { + console.log("// !!! beautify failed !!!"); + console.log(e.stack); } - var beautify_result = run_code(beautify_code); - - try { - var uglify_code = minify(beautify_code, { - fromString: true, - mangle: true, - compress: { - passes: 3, - }, - output: { - //beautify: true, - //bracketize: true, - }, - }).code; - } catch(e) { - parse_error = 2; - } - var uglify_result = run_code(uglify_code); - - var ok = !parse_error && original_result == beautify_result && original_result == uglify_result; - if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(ok); - if (parse_error === 1) console.log('Parse error while beautifying'); - if (parse_error === 2) console.log('Parse error while uglifying'); - if (!ok) break; + console.log("//"); + console.log(code); +} + +function infer_options(ctor) { + try { + ctor({ 0: 0 }); + } catch (e) { + return e.defs; + } +} + +var default_options = { + compress: infer_options(UglifyJS.Compressor), + mangle: { + "cache": null, + "eval": false, + "keep_fnames": false, + "screw_ie8": true, + "toplevel": false, + }, + output: infer_options(UglifyJS.OutputStream), +}; + +function log_suspects(minify_options, component) { + var options = component in minify_options ? minify_options[component] : true; + if (!options) return; + options = UglifyJS.defaults(options, default_options[component]); + var suspects = Object.keys(default_options[component]).filter(function(name) { + if (options[name]) { + var m = JSON.parse(JSON.stringify(minify_options)); + var o = JSON.parse(JSON.stringify(options)); + o[name] = false; + m[component] = o; + try { + var r = sandbox.run_code(UglifyJS.minify(original_code, m).code); + return sandbox.same_stdout(original_result, r); + } catch (e) { + console.log("Error testing options." + component + "." + name); + console.log(e); + } + } + }); + if (suspects.length > 0) { + console.log("Suspicious", component, "options:"); + suspects.forEach(function(name) { + console.log(" " + name); + }); + console.log(); + } +} + +function log(options) { + if (!ok) console.log('\n\n\n\n\n\n!!!!!!!!!!\n\n\n'); + console.log("//============================================================="); + if (!ok) console.log("// !!!!!! Failed... round", round); + console.log("// original code"); + try_beautify(original_code, original_result); + console.log(); + console.log(); + console.log("//-------------------------------------------------------------"); + if (typeof uglify_code == "string") { + console.log("// uglified code"); + try_beautify(uglify_code, uglify_result); + console.log(); + console.log(); + console.log("original result:"); + console.log(original_result); + console.log("uglified result:"); + console.log(uglify_result); + } else { + console.log("// !!! uglify failed !!!"); + console.log(uglify_code.stack); + } + console.log("minify(options):"); + options = JSON.parse(options); + console.log(options); + console.log(); + if (!ok) { + Object.keys(default_options).forEach(log_suspects.bind(null, options)); + console.log("!!!!!! Failed... round", round); + } +} + +var minify_options = require("./ufuzz.json").map(function(options) { + options.fromString = true; + return JSON.stringify(options); +}); +var original_code, original_result; +var uglify_code, uglify_result, ok; +for (var round = 1; round <= num_iterations; round++) { + process.stdout.write(round + " of " + num_iterations + "\r"); + + VAR_NAMES.length = INITIAL_NAMES_LEN; // prune any previous names still in the list + loops = 0; + funcs = 0; + + original_code = [ + "var a = 100, b = 10, c = 0;", + createTopLevelCode(), + "console.log(null, a, b, c);" // preceding `null` makes for a cleaner output (empty string still shows up etc) + ].join("\n"); + + minify_options.forEach(function(options) { + try { + uglify_code = UglifyJS.minify(original_code, JSON.parse(options)).code; + } catch (e) { + uglify_code = e; + } + + ok = typeof uglify_code == "string"; + if (ok) { + original_result = sandbox.run_code(original_code); + uglify_result = sandbox.run_code(uglify_code); + 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); + }); } diff --git a/test/ufuzz.json b/test/ufuzz.json new file mode 100644 index 00000000..2d871e87 --- /dev/null +++ b/test/ufuzz.json @@ -0,0 +1,33 @@ +[ + { + "compress": { + "warnings": false + } + }, + { + "compress": { + "warnings": false + }, + "mangle": false + }, + { + "compress": false, + "mangle": true + }, + { + "compress": false, + "mangle": false, + "output": { + "beautify": true, + "bracketize": true + } + }, + { + "compress": { + "keep_fargs": false, + "passes": 3, + "pure_getters": true, + "warnings": false + } + } +] diff --git a/tools/node.js b/tools/node.js index 6568a741..147751ae 100644 --- a/tools/node.js +++ b/tools/node.js @@ -46,21 +46,21 @@ function read_source_map(code) { UglifyJS.minify = function(files, options) { options = UglifyJS.defaults(options, { - spidermonkey : false, - outSourceMap : null, - outFileName : null, - sourceRoot : null, - inSourceMap : null, - sourceMapUrl : null, - sourceMapInline : false, + compress : {}, fromString : false, - warnings : false, + inSourceMap : null, mangle : {}, mangleProperties : false, nameCache : null, + outFileName : null, output : null, - compress : {}, - parse : {} + outSourceMap : null, + parse : {}, + sourceMapInline : false, + sourceMapUrl : null, + sourceRoot : null, + spidermonkey : false, + warnings : false, }); UglifyJS.base54.reset();