diff --git a/lib/ast.js b/lib/ast.js index ba1330f4..cdf75d4d 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -214,12 +214,13 @@ var AST_LabeledStatement = DEFNODE("LabeledStatement", "label", { clone: function(deep) { var node = this._clone(deep); if (deep) { - var refs = node.label.references; - var label = this.label; + var label = node.label; + var def = this.label; node.walk(new TreeWalker(function(node) { if (node instanceof AST_LoopControl - && node.label && node.label.thedef === label) { - refs.push(node); + && node.label && node.label.thedef === def) { + node.label.thedef = label; + label.references.push(node); } })); } diff --git a/lib/compress.js b/lib/compress.js index 1d9258cf..8454a432 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -271,6 +271,14 @@ merge(Compressor.prototype, { if (d.fixed === undefined || !is_safe(d) || is_modified(node, 0, node.fixed_value() instanceof AST_Lambda)) { d.fixed = false; + } else { + var parent = tw.parent(); + if (parent instanceof AST_Assign && parent.operator == "=" && node === parent.right + || parent instanceof AST_Call && node !== parent.expression + || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope + || parent instanceof AST_VarDef && node === parent.value) { + d.escaped = true; + } } } if (node instanceof AST_SymbolCatch) { @@ -359,7 +367,19 @@ merge(Compressor.prototype, { pop(); return true; } - if (node instanceof AST_Catch || node instanceof AST_SwitchBranch) { + if (node instanceof AST_Try) { + push(); + walk_body(node, tw); + pop(); + if (node.bcatch) { + push(); + node.bcatch.walk(tw); + pop(); + } + if (node.bfinally) node.bfinally.walk(tw); + return true; + } + if (node instanceof AST_SwitchBranch) { push(); descend(); pop(); @@ -393,6 +413,7 @@ merge(Compressor.prototype, { } function reset_def(def) { + def.escaped = false; if (toplevel || !def.global || def.orig[0] instanceof AST_SymbolConst) { def.fixed = undefined; } else { @@ -1549,23 +1570,20 @@ merge(Compressor.prototype, { : ev(this.alternative, compressor); }); def(AST_SymbolRef, function(compressor){ - if (this._evaluating) throw def; + if (!compressor.option("reduce_vars") || this._evaluating) throw def; this._evaluating = true; try { var fixed = this.fixed_value(); - if (compressor.option("reduce_vars") && fixed) { - if (compressor.option("unsafe")) { - if (!HOP(fixed, "_evaluated")) { - fixed._evaluated = ev(fixed, compressor); - } - return fixed._evaluated; - } - return ev(fixed, compressor); - } + if (!fixed) throw def; + var value = ev(fixed, compressor); + if (!HOP(fixed, "_eval")) fixed._eval = function() { + return value; + }; + if (value && typeof value == "object" && this.definition().escaped) throw def; + return value; } finally { this._evaluating = false; } - throw def; }); def(AST_PropAccess, function(compressor){ if (compressor.option("unsafe")) { @@ -1980,7 +1998,7 @@ merge(Compressor.prototype, { } return node; } - if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { + if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn && tt.parent().init === node)) { var def = node.definitions.filter(function(def){ if (def.value) def.value = def.value.transform(tt); var sym = def.name.definition(); @@ -2058,26 +2076,32 @@ merge(Compressor.prototype, { return maintain_this_binding(tt.parent(), node, node.right.transform(tt)); } } + // certain combination of unused name + side effect leads to: + // https://github.com/mishoo/UglifyJS2/issues/44 + // https://github.com/mishoo/UglifyJS2/issues/1830 + // that's an invalid AST. + // We fix it at this stage by moving the `var` outside the `for`. if (node instanceof AST_For) { descend(node, this); - if (node.init instanceof AST_BlockStatement) { - // certain combination of unused name + side effect leads to: - // https://github.com/mishoo/UglifyJS2/issues/44 - // that's an invalid AST. - // We fix it at this stage by moving the `var` outside the `for`. - - var body = node.init.body.slice(0, -1); - node.init = node.init.body.slice(-1)[0].body; - body.push(node); - - return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { - body: body - }); + var block = node.init; + node.init = block.body.pop(); + block.body.push(node); + return in_list ? MAP.splice(block.body) : block; } else if (is_empty(node.init)) { node.init = null; - return node; } + return node; + } + if (node instanceof AST_LabeledStatement && node.body instanceof AST_For) { + descend(node, this); + if (node.body instanceof AST_BlockStatement) { + var block = node.body; + node.body = block.body.pop(); + block.body.push(node); + return in_list ? MAP.splice(block.body) : block; + } + return node; } if (node instanceof AST_Scope && node !== self) return node; @@ -2380,7 +2404,7 @@ merge(Compressor.prototype, { 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 }); + return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } if (self instanceof AST_Do) { var has_loop_control = false; @@ -2389,7 +2413,8 @@ merge(Compressor.prototype, { if (node instanceof AST_LoopControl && tw.loopcontrol_target(node) === self) return has_loop_control = true; }); - self.walk(tw); + var parent = compressor.parent(); + (parent instanceof AST_LabeledStatement ? parent : self).walk(tw); if (!has_loop_control) return self.body; } } @@ -2459,7 +2484,7 @@ merge(Compressor.prototype, { })); } extract_declarations_from_unreachable_code(compressor, self.body, a); - return make_node(AST_BlockStatement, self, { body: a }); + return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } if (cond !== self.condition) { cond = make_node_from_constant(cond, self.condition).transform(compressor); @@ -2711,9 +2736,9 @@ merge(Compressor.prototype, { var body = []; if (self.bcatch) extract_declarations_from_unreachable_code(compressor, self.bcatch, body); if (self.bfinally) body = body.concat(self.bfinally.body); - return body.length > 0 ? make_node(AST_BlockStatement, self, { + return make_node(AST_BlockStatement, self, { body: body - }).optimize(compressor) : make_node(AST_EmptyStatement, self); + }).optimize(compressor); } return self; }); @@ -3595,7 +3620,9 @@ merge(Compressor.prototype, { return make_node(AST_Infinity, self).optimize(compressor); } } - if (compressor.option("evaluate") && compressor.option("reduce_vars")) { + if (compressor.option("evaluate") + && compressor.option("reduce_vars") + && is_lhs(self, compressor.parent()) !== self) { var d = self.definition(); var fixed = self.fixed_value(); if (fixed) { @@ -3603,21 +3630,45 @@ merge(Compressor.prototype, { 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 value = init.optimize(compressor).print_to_string().length; + var fn; + if (has_symbol_ref(fixed)) { + fn = function() { + var result = init.optimize(compressor); + return result === init ? result.clone(true) : result; + }; + } else { + value = Math.min(value, fixed.print_to_string().length); + fn = function() { + var result = best_of_expression(init.optimize(compressor), fixed); + return result === init || result === fixed ? result.clone(true) : result; + }; + } var name = d.name.length; - var freq = d.references.length; - var overhead = d.global || !freq ? 0 : (name + 2 + value) / freq; - d.should_replace = value <= name + overhead ? init : false; + var overhead = 0; + if (compressor.option("unused") && (!d.global || compressor.option("toplevel"))) { + overhead = (name + 2 + value) / d.references.length; + } + d.should_replace = value <= name + overhead ? fn : false; } else { d.should_replace = false; } } if (d.should_replace) { - return best_of_expression(d.should_replace.optimize(compressor), fixed).clone(true); + return d.should_replace(); } } } return self; + + function has_symbol_ref(value) { + var found; + value.walk(new TreeWalker(function(node) { + if (node instanceof AST_SymbolRef) found = true; + if (found) return true; + })); + return found; + } }); function is_atomic(lhs, self) { diff --git a/lib/output.js b/lib/output.js index 9ac50c08..0731fb49 100644 --- a/lib/output.js +++ b/lib/output.js @@ -190,11 +190,7 @@ function OutputStream(options) { var might_need_space = false; var might_need_semicolon = false; var might_add_newline = 0; - var last = null; - - function last_char() { - return last.charAt(last.length - 1); - }; + var last = ""; var ensure_line_len = options.max_line_len ? function() { if (current_col > options.max_line_len) { @@ -218,10 +214,11 @@ function OutputStream(options) { function print(str) { str = String(str); var ch = str.charAt(0); + var prev = last.charAt(last.length - 1); if (might_need_semicolon) { might_need_semicolon = false; - if ((!ch || ";}".indexOf(ch) < 0) && !/[;]$/.test(last)) { + if (prev == ":" && ch == "}" || (!ch || ";}".indexOf(ch) < 0) && prev != ";") { if (options.semicolons || requireSemicolonChars(ch)) { OUTPUT += ";"; current_col++; @@ -258,7 +255,6 @@ function OutputStream(options) { } if (might_need_space) { - var prev = last_char(); if ((is_identifier_char(prev) && (is_identifier_char(ch) || ch == "\\")) || (ch == "/" && ch == prev) diff --git a/test/benchmark.js b/test/benchmark.js index c150e5cf..561a9579 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -11,8 +11,8 @@ if (!args.length) { } args.push("--stats"); var urls = [ - "https://code.jquery.com/jquery-3.1.1.js", - "https://code.angularjs.org/1.6.1/angular.js", + "https://code.jquery.com/jquery-3.2.1.js", + "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.4/angular.js", "https://cdnjs.cloudflare.com/ajax/libs/mathjs/3.9.0/math.js", "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js", "https://unpkg.com/react@15.3.2/dist/react.js", diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 99d9cace..9d37dc7e 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -1029,3 +1029,38 @@ delete_assign_2: { } expect_stdout: true } + +issue_1830_1: { + options = { + unused: true, + } + input: { + !function() { + L: for (var b = console.log(1); !1;) continue L; + }(); + } + expect: { + !function() { + L: for (console.log(1); !1;) continue L; + }(); + } + expect_stdout: "1" +} + +issue_1830_2: { + options = { + unused: true, + } + input: { + !function() { + L: for (var a = 1, b = console.log(a); --a;) continue L; + }(); + } + expect: { + !function() { + var a = 1; + L: for (console.log(a); --a;) continue L; + }(); + } + expect_stdout: "1" +} diff --git a/test/compress/functions.js b/test/compress/functions.js index dca40623..7d1928cb 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -93,3 +93,57 @@ issue_485_crashing_1530: { this, void 0; } } + +issue_1841_1: { + options = { + keep_fargs: false, + pure_getters: "strict", + reduce_vars: true, + unused: true, + } + input: { + var b = 10; + !function(arg) { + for (var key in "hi") + var n = arg.baz, n = [ b = 42 ]; + }(--b); + console.log(b); + } + expect: { + var b = 10; + !function() { + for (var key in "hi") { + b = 42; + } + }(--b); + console.log(b); + } + expect_exact: "42" +} + +issue_1841_2: { + options = { + keep_fargs: false, + pure_getters: false, + reduce_vars: true, + unused: true, + } + input: { + var b = 10; + !function(arg) { + for (var key in "hi") + var n = arg.baz, n = [ b = 42 ]; + }(--b); + console.log(b); + } + expect: { + var b = 10; + !function(arg) { + for (var key in "hi") { + arg.baz, b = 42; + } + }(--b); + console.log(b); + } + expect_exact: "42" +} diff --git a/test/compress/issue-1656.js b/test/compress/issue-1656.js index 8b683a28..c4c8f863 100644 --- a/test/compress/issue-1656.js +++ b/test/compress/issue-1656.js @@ -35,11 +35,11 @@ f7: { console.log(a, b); } expect_exact: [ - "var a = 100, b = 10;", + "var b = 10;", "", "!function() {", - " for (;b = a, !1; ) ;", - "}(), console.log(a, b);", + " for (;b = 100, !1; ) ;", + "}(), console.log(100, b);", ] expect_stdout: true } diff --git a/test/compress/issue-1833.js b/test/compress/issue-1833.js new file mode 100644 index 00000000..e46dd046 --- /dev/null +++ b/test/compress/issue-1833.js @@ -0,0 +1,134 @@ +iife_for: { + options = { + negate_iife: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + function g() { + L: for (;;) break L; + } + g(); + } + f(); + } + expect: { + !function() { + !function() { + L: for (;;) break L; + }(); + }(); + } +} + +iife_for_in: { + options = { + negate_iife: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + function g() { + L: for (var a in x) break L; + } + g(); + } + f(); + } + expect: { + !function() { + !function() { + L: for (var a in x) break L; + }(); + }(); + } +} + +iife_do: { + options = { + negate_iife: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + function g() { + L: do { + break L; + } while (1); + } + g(); + } + f(); + } + expect: { + !function() { + !function() { + L: do { + break L; + } while (1); + }(); + }(); + } +} + +iife_while: { + options = { + negate_iife: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + function g() { + L: while (1) break L; + } + g(); + } + f(); + } + expect: { + !function() { + !function() { + L: while (1) break L; + }(); + }(); + } +} + +label_do: { + options = { + evaluate: true, + loops: true, + } + input: { + L: do { + continue L; + } while (0); + } + expect: { + L: do { + continue L; + } while (0); + } +} + +label_while: { + options = { + evaluate: true, + dead_code: true, + loops: true, + } + input: { + function f() { + L: while (0) continue L; + } + } + expect_exact: "function f(){L:;}" +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index b6f711ad..6726bab5 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -300,7 +300,7 @@ unsafe_evaluate_array: { } } -unsafe_evaluate_equality: { +unsafe_evaluate_equality_1: { options = { evaluate : true, reduce_vars : true, @@ -308,47 +308,62 @@ unsafe_evaluate_equality: { unused : true } input: { - function f0(){ + function f0() { var a = {}; - console.log(a === a); + return a === a; } - - function f1(){ + function f1() { var a = []; - console.log(a === a); + return a === a; } + console.log(f0(), f1()); + } + expect: { + function f0() { + return true; + } + function f1() { + return true; + } + console.log(f0(), f1()); + } + expect_stdout: true +} - function f2(){ +unsafe_evaluate_equality_2: { + options = { + collapse_vars: true, + evaluate : true, + passes : 2, + reduce_vars : true, + unsafe : true, + unused : true + } + input: { + function f2() { var a = {a:1, b:2}; var b = a; var c = a; - console.log(b === c); + return b === c; } - - function f3(){ + function f3() { var a = [1, 2, 3]; var b = a; var c = a; - console.log(b === c); + return b === c; } + console.log(f2(), f3()); } expect: { - function f0(){ - console.log(true); + function f2() { + return true; } - - function f1(){ - console.log(true); - } - - function f2(){ - console.log(true); - } - - function f3(){ - console.log(true); + function f3() { + return true; } + console.log(f2(), f3()); } + expect_stdout: true } passes: { @@ -1995,3 +2010,115 @@ catch_var: { } expect_stdout: "true" } + +issue_1814_1: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + const a = 42; + !function() { + var b = a; + !function(a) { + console.log(a++, b); + }(0); + }(); + } + expect: { + const a = 42; + !function() { + !function(a) { + console.log(a++, 42); + }(0); + }(); + } + expect_stdout: "0 42" +} + +issue_1814_2: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + const a = "32"; + !function() { + var b = a + 1; + !function(a) { + console.log(a++, b); + }(0); + }(); + } + expect: { + const a = "32"; + !function() { + !function(a) { + console.log(a++, "321"); + }(0); + }(); + } + expect_stdout: "0 '321'" +} + +try_abort: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + !function() { + try { + var a = 1; + throw ""; + var b = 2; + } catch (e) { + } + console.log(a, b); + }(); + } + expect: { + !function() { + try { + var a = 1; + throw ""; + var b = 2; + } catch (e) { + } + console.log(a, b); + }(); + } + expect_stdout: "1 undefined" +} + +issue_1865: { + options = { + evaluate: true, + reduce_vars: true, + unsafe: true, + } + input: { + function f(some) { + some.thing = false; + } + console.log(function() { + var some = { thing: true }; + f(some); + return some.thing; + }()); + } + expect: { + function f(some) { + some.thing = false; + } + console.log(function() { + var some = { thing: true }; + f(some); + return some.thing; + }()); + } + expect_stdout: true +} diff --git a/test/sandbox.js b/test/sandbox.js index 894349fb..eb9f1f0f 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -1,15 +1,35 @@ var vm = require("vm"); +function safe_log(arg) { + if (arg) switch (typeof arg) { + case "function": + return arg.toString(); + case "object": + if (/Error$/.test(arg.name)) return arg.toString(); + arg.constructor.toString(); + for (var key in arg) { + arg[key] = safe_log(arg[key]); + } + } + return arg; +} + var FUNC_TOSTRING = [ "Function.prototype.toString = Function.prototype.valueOf = function() {", - " var ids = [];", + " var id = 0;", " return function() {", - " var i = ids.indexOf(this);", - " if (i < 0) {", - " i = ids.length;", - " ids.push(this);", + ' if (this === Array) return "[Function: Array]";', + ' if (this === Object) return "[Function: Object]";', + " var i = this.name;", + ' if (typeof i != "number") {', + " i = ++id;", + ' Object.defineProperty(this, "name", {', + " get: function() {", + " return i;", + " }", + " });", " }", - ' return "[Function: __func_" + i + "__]";', + ' return "[Function: " + i + "]";', " }", "}();", ].join("\n"); @@ -21,16 +41,14 @@ exports.run_code = function(code) { }; try { vm.runInNewContext([ - "!function() {", FUNC_TOSTRING, + "!function() {", code, "}();", ].join("\n"), { console: { log: function() { - return console.log.apply(console, [].map.call(arguments, function(arg) { - return typeof arg == "function" || arg && /Error$/.test(arg.name) ? arg.toString() : arg; - })); + return console.log.apply(console, [].map.call(arguments, safe_log)); } } }, { timeout: 5000 }); diff --git a/test/ufuzz.js b/test/ufuzz.js index 2a09e2f7..22f17c35 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -2,7 +2,7 @@ // 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. +// 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 @@ -20,49 +20,26 @@ 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_ARG_TO_ID = Object.create(null); +var STMTS_TO_USE = []; +function STMT_(name) { + return STMT_ARG_TO_ID[name] = STMTS_TO_USE.push(STMTS_TO_USE.length) - 1; +} + +var STMT_BLOCK = STMT_("block"); +var STMT_IF_ELSE = STMT_("ifelse"); +var STMT_DO_WHILE = STMT_("dowhile"); +var STMT_WHILE = STMT_("while"); +var STMT_FOR_LOOP = STMT_("forloop"); +var STMT_FOR_IN = STMT_("forin"); +var STMT_SEMI = STMT_("semi"); +var STMT_EXPR = STMT_("expr"); +var STMT_SWITCH = STMT_("switch"); +var STMT_VAR = STMT_("var"); +var STMT_RETURN_ETC = STMT_("stop"); +var STMT_FUNC_EXPR = STMT_("funcexpr"); +var STMT_TRY = STMT_("try"); +var STMT_C = STMT_("c"); var STMT_FIRST_LEVEL_OVERRIDE = -1; var STMT_SECOND_LEVEL_OVERRIDE = -1; @@ -72,6 +49,7 @@ 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; +var use_strict = false; for (var i = 2; i < process.argv.length; ++i) { switch (process.argv[i]) { case '-v': @@ -101,6 +79,9 @@ for (var i = 2; i < process.argv.length; ++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 '--use-strict': + use_strict = true; + break; case '--stmt-depth-from-func': STMT_COUNT_FROM_GLOBAL = false; break; @@ -127,6 +108,7 @@ for (var i = 2; i < process.argv.length; ++i) { 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('--use-strict: generate "use strict"'); 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'); @@ -296,15 +278,26 @@ var TYPEOF_OUTCOMES = [ var loops = 0; var funcs = 0; +var labels = 10000; function rng(max) { var r = randomBytes(2).readUInt16LE(0) / 65536; return Math.floor(max * r); } +function strictMode() { + return use_strict && rng(4) == 0 ? '"use strict";' : ''; +} + 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); + return [ + strictMode(), + 'var a = 100, b = 10, c = 0;', + rng(2) == 0 + ? createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0) + : createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, IN_GLOBAL, ANY_TYPE, CANNOT_THROW, 0), + 'console.log(null, a, b, c);' // preceding `null` makes for a cleaner output (empty string still shows up etc) + ].join('\n'); } function createFunctions(n, recurmax, inGlobal, noDecl, canThrow, stmtDepth) { @@ -342,10 +335,22 @@ 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 + '(' + createParams() + '){' + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth) + '}\n'; + s = [ + 'function ' + name + '(' + createParams() + '){', + strictMode(), + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth), + '}', + '' + ].join('\n'); } else { // functions with statements - s = 'function ' + name + '(' + createParams() + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; + s = [ + 'function ' + name + '(' + createParams() + '){', + strictMode(), + createStatements(3, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '}', + '' + ].join('\n'); } VAR_NAMES.length = namesLenBefore; @@ -367,6 +372,40 @@ function createStatements(n, recurmax, canThrow, canBreak, canContinue, cannotRe return s; } +function enableLoopControl(flag, defaultValue) { + return Array.isArray(flag) && flag.indexOf("") < 0 ? flag.concat("") : flag || defaultValue; +} + +function createLabel(canBreak, canContinue) { + var label; + if (rng(10) < 3) { + label = ++labels; + if (Array.isArray(canBreak)) { + canBreak = canBreak.slice(); + } else { + canBreak = canBreak ? [ "" ] : []; + } + canBreak.push(label); + if (Array.isArray(canContinue)) { + canContinue = canContinue.slice(); + } else { + canContinue = canContinue ? [ "" ] : []; + } + canContinue.push(label); + } + return { + break: canBreak, + continue: canContinue, + target: label ? "L" + label + ": " : "" + }; +} + +function getLabel(label) { + if (!Array.isArray(label)) return ""; + label = label[rng(label.length)]; + return label && " L" + label; +} + function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { ++stmtDepth; var loop = ++loops; @@ -382,17 +421,36 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn switch (target) { case STMT_BLOCK: - return '{' + createStatements(rng(5) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '}'; + var label = createLabel(canBreak); + return label.target + '{' + createStatements(rng(5) + 1, recurmax, canThrow, label.break, 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);}'; + var label = createLabel(canBreak, canContinue); + canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); + canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); + return '{var brake' + loop + ' = 5; ' + label.target + 'do {' + createStatement(recurmax, canThrow, canBreak, canContinue, 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) + '}'; + var label = createLabel(canBreak, canContinue); + canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); + canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); + return '{var brake' + loop + ' = 5; ' + label.target + 'while ((' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && --brake' + loop + ' > 0)' + createStatement(recurmax, canThrow, canBreak, canContinue, 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); + var label = createLabel(canBreak, canContinue); + canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); + canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); + return label.target + 'for (var brake' + loop + ' = 5; (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && brake' + loop + ' > 0; --brake' + loop + ')' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth); + case STMT_FOR_IN: + var label = createLabel(canBreak, canContinue); + canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); + canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); + var optElementVar = ''; + if (rng(5) > 1) { + optElementVar = 'c = 1 + c; var ' + createVarName(MANDATORY) + ' = expr' + loop + '[key' + loop + ']; '; + } + return '{var expr' + loop + ' = ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + '; ' + label.target + ' for (var key' + loop + ' in expr' + loop + ') {' + optElementVar + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '}}'; case STMT_SEMI: - return ';'; + return use_strict && rng(20) === 0 ? '"use strict";' : ';'; case STMT_EXPR: return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';'; case STMT_SWITCH: @@ -424,8 +482,8 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn case 1: case 2: case 3: - if (canBreak && rng(5) === 0) return 'break;'; - if (canContinue && rng(5) === 0) return 'continue;'; + if (canBreak && rng(5) === 0) return 'break' + getLabel(canBreak) + ';'; + if (canContinue && rng(5) === 0) return 'continue' + getLabel(canContinue) + ';'; if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; if (rng(3) == 0) return '/*3*/return;'; return 'return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; @@ -470,25 +528,27 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { var hadDefault = false; - var s = ''; + var s = ['']; + canBreak = enableLoopControl(canBreak, CAN_BREAK); 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'; + s.push( + 'case ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':', + createStatements(rng(3) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth), + rng(10) > 0 ? ' break;' : '/* fall-through */', + '' + ); } else { hadDefault = true; - s += '' + - 'default:\n' + - createStatements(rng(3) + 1, recurmax, canThrow, CAN_BREAK, canContinue, cannotReturn, stmtDepth) + - '\n'; + s.push( + 'default:', + createStatements(rng(3) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth), + '' + ); } } - return s; + return s.join('\n'); } function createExpression(recurmax, noComma, stmtDepth, canThrow) { @@ -530,37 +590,66 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { 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 p++: 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'; - var s = ''; - switch(rng(4)) { + if (name == 'c') name = 'a'; + var s = []; + switch (rng(5)) { case 0: - s = '(function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '})()'; + s.push( + '(function ' + name + '(){', + strictMode(), + 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) + '}()'; + s.push( + '+function ' + name + '(){', + strictMode(), + 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) + '}()'; + s.push( + '!function ' + name + '(){', + strictMode(), + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '}()' + ); + break; + case 3: + s.push( + 'void function ' + name + '(){', + strictMode(), + 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) + '}()'; + var instantiate = rng(4) ? 'new ' : ''; + s.push( + instantiate + 'function ' + name + '(){', + strictMode() + ); + if (instantiate) for (var i = rng(4); --i >= 0;) { + if (rng(2)) s.push('this.' + getDotKey() + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ';'); + else s.push('this[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']' + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ';'); + } + s.push( + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '}' + ); break; } VAR_NAMES.length = nameLenBefore; - return s; + return s.join('\n'); case p++: case p++: return createTypeofExpr(recurmax, stmtDepth, canThrow); - 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? @@ -682,22 +771,23 @@ function _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { function _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { // intentionally generate more hardcore ops if (--recurmax < 0) return createValue(); + var assignee, expr; 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(); + assignee = getVarName(); return '(' + assignee + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; case 3: - var assignee = getVarName(); - var expr = '(' + assignee + '[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + assignee = getVarName(); + 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() + assignee = getVarName(); + expr = '(' + assignee + '.' + getDotKey() + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; return canThrow && rng(10) == 0 ? expr : '(' + assignee + ' && ' + expr + ')'; default: @@ -802,8 +892,8 @@ var default_options = { mangle: { "cache": null, "eval": false, + "ie8": false, "keep_fnames": false, - "screw_ie8": true, "toplevel": false, }, output: infer_options(UglifyJS.OutputStream), @@ -858,17 +948,27 @@ function log(options) { } else { console.log("// !!! uglify failed !!!"); console.log(uglify_code.stack); + if (typeof original_result != "string") { + console.log(); + console.log(); + console.log("original stacktrace:"); + console.log(original_result.stack); + } } console.log("minify(options):"); options = JSON.parse(options); console.log(options); console.log(); - if (!ok) { + if (!ok && typeof uglify_code == "string") { Object.keys(default_options).forEach(log_suspects.bind(null, options)); console.log("!!!!!! Failed... round", round); } } +var fallback_options = [ JSON.stringify({ + compress: false, + mangle: false +}) ]; var minify_options = require("./ufuzz.json").map(function(options) { options.fromString = true; return JSON.stringify(options); @@ -882,13 +982,9 @@ for (var round = 1; round <= num_iterations; round++) { 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) { + original_code = createTopLevelCode(); + original_result = sandbox.run_code(original_code); + (typeof original_result != "string" ? fallback_options : minify_options).forEach(function(options) { try { uglify_code = UglifyJS.minify(original_code, JSON.parse(options)).code; } catch (e) { @@ -897,9 +993,10 @@ for (var round = 1; round <= num_iterations; round++) { 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); + } else if (typeof original_result != "string") { + ok = uglify_code.name == original_result.name; } if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(options); else if (verbose_error && typeof original_result != "string") {