From 11e9bdc42746832cfa1c52ca70b0171b31a422ba Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 31 Mar 2017 15:26:57 +0800 Subject: [PATCH 01/17] fix missing preamble when shebang is absent (#1742) --- lib/output.js | 4 ++-- test/mocha/comment-filter.js | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/output.js b/lib/output.js index c8c8739f..ac9e0654 100644 --- a/lib/output.js +++ b/lib/output.js @@ -510,8 +510,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(); } 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;"); + }); }); From f8a71b56fd6979028d047ce2822878e9796fcec6 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 31 Mar 2017 15:27:40 +0800 Subject: [PATCH 02/17] v2.8.20 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90172312..ffac7c67 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.20", "engines": { "node": ">=0.8.0" }, From a0c3836ba0e3baca8d758d9855144ac15ec0ac8d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 31 Mar 2017 16:41:04 +0800 Subject: [PATCH 03/17] sort options in alphabetical order (#1743) They started off as functional groups I guess, but given the sheer number of options this is becoming too difficult to read. --- lib/compress.js | 56 +++++++++++++++++++++++------------------------ lib/output.js | 30 ++++++++++++------------- lib/parse.js | 12 +++++----- lib/propmangle.js | 12 +++++----- lib/scope.js | 14 ++++++------ tools/node.js | 20 ++++++++--------- 6 files changed, 72 insertions(+), 72 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index ac7ea357..55f6d793 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -48,42 +48,42 @@ 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, + 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, + 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, + 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, - 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") { diff --git a/lib/output.js b/lib/output.js index ac9e0654..d5c7304a 100644 --- a/lib/output.js +++ b/lib/output.js @@ -53,26 +53,26 @@ 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, - 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, + 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, + source_map : null, + space_colon : true, + unescape_regexps : false, + width : 80, wrap_iife : false, }, true); diff --git a/lib/parse.js b/lib/parse.js index 1ccde26a..c34e13db 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -688,14 +688,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 3c75cac9..cfa035d2 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -62,12 +62,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 df0a7e00..74760e4f 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -92,8 +92,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 @@ -398,12 +398,12 @@ AST_Symbol.DEFMETHOD("global", function(){ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { - except : [], eval : false, + except : [], + keep_fnames : false, + screw_ie8 : true, sort : false, // Ignored. Flag retained for backwards compatibility. toplevel : false, - screw_ie8 : true, - keep_fnames : false }); }); @@ -576,12 +576,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/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(); From e6b76a48790cc3659c53673d0cc2a962acec3379 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 31 Mar 2017 11:23:50 +0200 Subject: [PATCH 04/17] Massive extension of the fuzzer (#1697) Fix bug where a `throw` was generated without expression Reenable try/catch/finally and fix them up Skip serialization errors Allow function decl in other funcs but not in blocks etc Rename function to be more appropriate Fix global functions not getting certain names Make the canaries more likely to appear as expressions Add a silly rounding edge case Add a new canary, `c`, which should only ever be incremented Refactoring Fix (another) iife not actually being invoked When a statement hits recursion max return an expression instead of `;` When a expression hits recursion max also inc `c` Generate global code as well as function code Also fixes some argument juggling related bugs. No longer reduces the recursion max when generating sub functions. Generates a function arg. Add used names to var name pool while in that scope This is a little wonky, possibly a hack, but since it's synchronous code I think it's alright to do this. The alternative is to slice the varnames array and juggle them through almost all the generator functions and there are various reasons why this patch is a better alternative. Minify generated code, not beautified code. Prevents beautifier bias. Prevent unnecessary duplication Remove serialization protection because I think it got handled elsewhere Abstract toplevel code generation Add example line of running test case Add poor man options parser, and some options Reindent to 4 spaces Lower chance of `default` generation Comment example of testing a case and output improvement Enable `default` clause appearing at any clause index Removing some training wheels; dont add parens where we dont absolutely need them Support `-s1` and `-s2` to force specific statements being generated at that recursion level Add round number to output when failing. For stats and fun and profit. Solidify statement depth counting. The argument juggling is real. Renamed option to something long. -scf was ugly and probably confusing. Fix missing arguments causing `canThrow` to be truthy, generating crashing code Generate more binary nested expressions Add black and white list cli options for statement generation Allows you to explicitly require or forbid certain statements from/to being made. ``` node test/ufuzz.js --without-stmt switch,try -t 5 -r 5 -V ``` ``` node test/ufuzz.js --only-stmt ifelse,expr -t 5 -r 5 -V ``` Similar granularity for expression may be added later. There can be no comma between names; it just does a split on that arg. Trim down the binary expression generator Prevent scoping issues in nodejs by preventing certain names in global space Oh this list was incomplete? Allow bin-expr to generate assignments too. More vigilant with storing and reusing vars. Add more global builtin names Update wrapper code Also patch Function valueOf --- test/ufuzz.js | 910 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 587 insertions(+), 323 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index c1ac8f4c..c1866175 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -2,6 +2,10 @@ // 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) @@ -11,118 +15,264 @@ var vm = require("vm"); var minify = require("..").minify; -var MAX_GENERATED_FUNCTIONS_PER_RUN = 1; -var MAX_GENERATION_RECURSION_DEPTH = 15; +var MAX_GENERATED_TOPLEVELS_PER_RUN = 3; +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 +var enable_beautifier = false; // run beautifier as well? +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 '-b': + enable_beautifier = 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 '-?': + 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('-b: also run beautifier'); + 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 -? 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', + '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"' ]; 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', + 'let' ]; // maybe omit this, it's more a parser problem than minifier +var INITIAL_NAMES_LEN = VAR_NAMES.length; var TYPEOF_OUTCOMES = [ - 'undefined', - 'string', - 'number', - 'object', - 'boolean', - 'special', - 'unknown', - 'symbol', - 'crap' ]; + 'undefined', + 'string', + 'number', + 'object', + 'boolean', + 'special', + 'unknown', + 'symbol', + 'crap' ]; var FUNC_TOSTRING = [ - "Function.prototype.toString = function() {", + "Function.prototype.toString = Function.prototype.valueOf = function() {", " var ids = [];", " return function() {", " var i = ids.indexOf(this);", @@ -135,6 +285,8 @@ var FUNC_TOSTRING = [ "}();", "" ].join("\n"); +var loops = 0; +var funcs = 0; function run_code(code) { var stdout = ""; @@ -147,11 +299,11 @@ function run_code(code) { console: { log: function() { return console.log.apply(console, [].map.call(arguments, function(arg) { - return typeof arg == "function" ? "[Function]" : arg; + return typeof arg == "function" ? arg.toString() : arg; })); } } - }, { timeout: 5000 }); + }, { timeout: 30000 }); return stdout; } catch (ex) { return ex; @@ -161,254 +313,357 @@ function run_code(code) { } function rng(max) { - return Math.floor(max * Math.random()); + 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; -} - -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; - } -} - -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 createTopLevelCodes(n) { + var s = ''; + while (n-- > 0) { + s += createTopLevelCode() + '\n\n//$$$$$$$$$$$$$$\n\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) + '}'; +function createTopLevelCode() { + var r = rng(3); + if (r > 0) return createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, IN_GLOBAL, ANY_TYPE, CANNOT_THROW, 0); + return createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0, IN_GLOBAL); +} + +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; +} + +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, inGlobal, 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) === 1) { + // functions with functions. lower the recursion to prevent a mess. + s = 'function ' + name + '(' + createVarName(MANDATORY, inGlobal) + '){' + 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, inGlobal) + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, inGlobal, NOT_GLOBAL) + '}\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 createStatements(n, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) { + if (--recurmax < 0) { return ';'; } + var s = ''; + while (--n > 0) { + s += createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) + '\n'; + } + return s; +} + +function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) { + ++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, inGlobal) + '}'; + case STMT_IF_ELSE: + return 'if (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) + (rng(2) === 1 ? ' else ' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) : ''); + case STMT_DO_WHILE: + return '{var brake' + loop + ' = 5; do {' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE, cannotReturn, stmtDepth, inGlobal) + '} 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, inGlobal) + '}'; + 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, inGlobal); + 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, inGlobal) + '}'; + case STMT_VAR: + switch (rng(3)) { + case 0: + var name = createVarName(MANDATORY, inGlobal); + if (name === 'c') name = 'a'; + return 'var ' + name + ';'; + case 1: + // initializer can only have one expression + var name = createVarName(MANDATORY, inGlobal); + 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, inGlobal); + if (n1 === 'c') n1 = 'b'; + var n2 = createVarName(MANDATORY, inGlobal); + 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, inGlobal) + ' }'; + 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, inGlobal) + ' }'; + if (freshCatchName) VAR_NAMES.splice(nameLenBefore, 1); // remove catch name + } + if (n !== 0) s += ' finally { ' + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) + ' }'; + return s; + case STMT_C: + return 'c = c + 1;'; 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)) { + throw 'no'; + } +} + +function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) { + 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, inGlobal) + + '\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, inGlobal) + + '\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(13)) { case 0: - return '(a/* ignore */++)'; + return createUnaryOp() + (rng(2) === 1 ? 'a' : 'b'); case 1: - return '(b/* ignore */--)'; + return 'a' + (rng(2) == 1 ? '++' : '--'); case 2: - return '(++/* ignore */a)'; + // 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 '(--/* ignore */b)'; + return rng(2) + ' === 1 ? a : 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)'; - } - } + 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, NOT_GLOBAL); // 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, NOT_GLOBAL) + '})()'; + break; + case 1: + s = '+function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, NOT_GLOBAL) + '}()'; + break; + case 2: + s = '!function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, NOT_GLOBAL) + '}()'; + break; + default: + s = 'void function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, NOT_GLOBAL) + '}()'; + break; + } + VAR_NAMES.length = nameLenBefore; + return s; + case 9: + return createTypeofExpr(); + 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); + } } -function createTypeofExpr(recurmax) { - if (--recurmax < 0) { - return 'typeof undefined === "undefined"'; - } +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; +} - 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 createTypeofExpr() { + switch (rng(5)) { + case 0: + return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + case 1: + return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE) + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + case 2: + return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE) + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + case 3: + return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + case 4: + return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE); + } } 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, inGlobal, dontStore) { + if (!maybe || rng(2) === 1) { + do { + var r = rng(VAR_NAMES.length); + var suffixed = rng(5) > 0; + var name = VAR_NAMES[r] + (suffixed ? '_' + (++loops) : ''); + } while (inGlobal && (name === 'NaN' || name === 'undefined' || name === 'Infinity')); // prevent nodejs module strict mode problems + if (!dontStore && suffixed) VAR_NAMES.push(name); + return name; + } + return ''; } function log(ok) { + if (!ok) console.log('\n\n\n\n\n\n!!!!!!!!!!\n\n\n'); console.log("//============================================================="); - if (!ok) console.log("// !!!!!! Failed..."); + if (!ok) console.log("// !!!!!! Failed... round", round); 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(); + if (enable_beautifier) { + 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("//"); @@ -417,43 +672,51 @@ function log(ok) { console.log(); console.log("original result:"); console.log(original_result); - console.log("beautified result:"); - console.log(beautify_result); + if (enable_beautifier) { + console.log("beautified result:"); + console.log(beautify_result); + } console.log("uglified result:"); console.log(uglify_result); - if (!ok) console.log("!!!!!! Failed..."); + if (!ok) console.log("!!!!!! Failed... round", round); } -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_NAMES.length = INITIAL_NAMES_LEN; // prune any previous names still in the list + loops = 0; + funcs = 0; + var original_code = [ - "var a = 100, b = 10;", - createFunctionDecls(rng(MAX_GENERATED_FUNCTIONS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH), - "console.log(a, b);" + "var a = 100, b = 10, c = 0;", + createTopLevelCodes(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1) + + "console.log(null, a, b, c);" // the array makes for a cleaner output (empty string still shows up etc) ].join("\n"); var original_result = run_code(original_code); - try { - var beautify_code = minify(original_code, { - fromString: true, - mangle: false, - compress: false, - output: { - beautify: true, - bracketize: true, - }, - }).code; - } catch(e) { - parse_error = 1; + if (enable_beautifier) { + try { + var beautify_code = minify(original_code, { + fromString: true, + mangle: false, + compress: false, + output: { + beautify: true, + bracketize: true, + }, + }).code; + } catch(e) { + parse_error = 1; + } + var beautify_result = run_code(beautify_code); + } else { + var beautify_result = original_result; } - var beautify_result = run_code(beautify_code); try { - var uglify_code = minify(beautify_code, { + var uglify_code = minify(original_code, { fromString: true, mangle: true, compress: { @@ -470,6 +733,7 @@ for (var round = 0; round < num_iterations; round++) { var uglify_result = run_code(uglify_code); var ok = !parse_error && original_result == beautify_result && original_result == uglify_result; + //if (!ok && typeof original_result === 'string' && original_result.indexOf('[Function:') >= 0) ok = true; 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'); From 1ddc05725d078ccf73d711e376c3c530cd517cdb Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 31 Mar 2017 18:47:44 +0800 Subject: [PATCH 05/17] combine rules for binary boolean operations (#1744) --- lib/compress.js | 90 ++++++++++++++++--------------------- test/compress/issue-1261.js | 4 +- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 55f6d793..a2332a9c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3140,42 +3140,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") { @@ -3192,7 +3157,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) @@ -3233,24 +3197,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; 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]", From 257ddc3bdb37efdb48fc23371f5f523e2044afd8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 1 Apr 2017 03:02:14 +0800 Subject: [PATCH 06/17] improve compression of undefined, NaN & Infinitiy (#1748) - migrate transformation logic from `OutputStream` to `Compressor` - always turn `undefined` into `void 0` (unless `unsafe`) - always keep `NaN` except when avoiding local variable redefinition - introduce `keep_infinity` to suppress `1/0` transform, except when avoiding local variable redefinition supersedes #1723 fixes #1730 --- README.md | 3 ++ lib/compress.js | 75 +++++++++++++++++++++++++++-------- lib/output.js | 28 +------------ test/compress/conditionals.js | 8 ++-- test/compress/evaluate.js | 8 ++-- test/compress/issue-1105.js | 68 ++++++++++++++++++++++++++++++- test/compress/issue-597.js | 32 ++++++++++++++- test/compress/numbers.js | 2 +- test/compress/properties.js | 2 +- 9 files changed, 170 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 2399e23f..d57a15ce 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 a2332a9c..1d9cd401 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -66,6 +66,7 @@ function Compressor(options, 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, @@ -215,7 +216,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) { @@ -1123,8 +1129,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 ]----- */ @@ -1313,7 +1323,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 @@ -2971,7 +2981,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 @@ -3010,7 +3020,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()) { @@ -3034,6 +3044,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 == "%")) { @@ -3043,8 +3056,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); @@ -3087,8 +3099,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); } @@ -3101,8 +3113,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. @@ -3464,9 +3476,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")) { @@ -3508,7 +3520,38 @@ merge(Compressor.prototype, { 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") + && !compressor.find_parent(AST_Scope).find_variable("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 compressor.find_parent(AST_Scope).find_variable("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 = [ '+', '-', '/', '*', '%', '>>', '<<', '>>>', '|', '^', '&' ]; @@ -3809,7 +3852,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; diff --git a/lib/output.js b/lib/output.js index d5c7304a..d0adbddf 100644 --- a/lib/output.js +++ b/lib/output.js @@ -586,21 +586,12 @@ 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; }); - 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) @@ -1258,24 +1249,7 @@ function OutputStream(options) { var def = self.definition(); output.print_name(def ? def.mangled_name || def.name : self.name); }); - 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/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 35b6b925..0d26e749 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; @@ -533,7 +533,7 @@ unsafe_array: { [1, 2, 3, a][0] + 1, 2, 3, - 0/0, + NaN, "1,21", 5, (void 0)[1] + 1 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-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 376fb9e2..29bdfe2a 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; } From c934fc81426b0f896d23499f50ba3e86fe9d7725 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 1 Apr 2017 05:47:11 +0800 Subject: [PATCH 07/17] implement `test/sandbox.js` (#1749) - `test/run-tests.js` and `test/ufuzz.js` now shares the same `run_code()` and `same_stdout()` - re-enable fuzzer to generate top-level `NaN`, `Infinity` & `undefined` - attempt to show beautified output only when `run_code()` output is preserved --- test/run-tests.js | 37 +-- test/sandbox.js | 50 ++++ test/ufuzz.js | 634 +++++++++++++++++++++------------------------- 3 files changed, 344 insertions(+), 377 deletions(-) create mode 100644 test/sandbox.js 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 c1866175..4889598b 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -12,8 +12,8 @@ stream._handle.setBlocking(true); }); -var vm = require("vm"); var minify = require("..").minify; +var sandbox = require("./sandbox"); var MAX_GENERATED_TOPLEVELS_PER_RUN = 3; var MAX_GENERATION_RECURSION_DEPTH = 12; @@ -70,69 +70,65 @@ var STMT_COUNT_FROM_GLOBAL = true; // count statement depth from nearest functio var num_iterations = +process.argv[2] || 1/0; var verbose = false; // log every generated test var verbose_interval = false; // log every 100 generated tests -var enable_beautifier = false; // run beautifier as well? 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 '-b': - enable_beautifier = 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 '-?': - 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('-b: also run beautifier'); - 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 -? for help'); + 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 '-?': + 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('-b: also run beautifier'); + 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 -? for help'); } } @@ -271,47 +267,9 @@ var TYPEOF_OUTCOMES = [ 'symbol', 'crap' ]; -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"); var loops = 0; var funcs = 0; -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" ? arg.toString() : arg; - })); - } - } - }, { timeout: 30000 }); - return stdout; - } catch (ex) { - return ex; - } finally { - process.stdout.write = original_write; - } -} - function rng(max) { return Math.floor(max * Math.random()); } @@ -327,7 +285,7 @@ function createTopLevelCodes(n) { function createTopLevelCode() { var r = rng(3); if (r > 0) return createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, IN_GLOBAL, ANY_TYPE, CANNOT_THROW, 0); - return createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0, IN_GLOBAL); + return createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0); } function createFunctions(n, recurmax, inGlobal, noDecl, canThrow, stmtDepth) { @@ -344,15 +302,15 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { 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, inGlobal, noDecl); + 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) === 1) { // functions with functions. lower the recursion to prevent a mess. - s = 'function ' + name + '(' + createVarName(MANDATORY, inGlobal) + '){' + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth) + '}\n'; + 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, inGlobal) + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, inGlobal, NOT_GLOBAL) + '}\n'; + s = 'function ' + name + '(' + createVarName(MANDATORY) + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; } VAR_NAMES.length = namesLenBefore; @@ -365,16 +323,16 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { return s; } -function createStatements(n, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) { +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, inGlobal) + '\n'; + s += createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '\n'; } return s; } -function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) { +function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { ++stmtDepth; var loop = ++loops; if (--recurmax < 0) { @@ -388,90 +346,90 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn 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, inGlobal) + '}'; - case STMT_IF_ELSE: - return 'if (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) + (rng(2) === 1 ? ' else ' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) : ''); - case STMT_DO_WHILE: - return '{var brake' + loop + ' = 5; do {' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE, cannotReturn, stmtDepth, inGlobal) + '} 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, inGlobal) + '}'; - 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, inGlobal); - 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, inGlobal) + '}'; - case STMT_VAR: - switch (rng(3)) { - case 0: - var name = createVarName(MANDATORY, inGlobal); - if (name === 'c') name = 'a'; - return 'var ' + name + ';'; - case 1: - // initializer can only have one expression - var name = createVarName(MANDATORY, inGlobal); - 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, inGlobal); - if (n1 === 'c') n1 = 'b'; - var n2 = createVarName(MANDATORY, inGlobal); - 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, inGlobal) + ' }'; - 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, inGlobal) + ' }'; - if (freshCatchName) VAR_NAMES.splice(nameLenBefore, 1); // remove catch name - } - if (n !== 0) s += ' finally { ' + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, inGlobal) + ' }'; - return s; - case STMT_C: - return 'c = c + 1;'; - default: - throw 'no'; + 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, inGlobal) { +function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { var hadDefault = false; var s = ''; while (n-- > 0) { @@ -479,7 +437,7 @@ function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotR 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, inGlobal) + + createStatements(rng(3) + 1, recurmax, canThrow, CAN_BREAK, canContinue, cannotReturn, stmtDepth) + '\n' + (rng(10) > 0 ? ' break;' : '/* fall-through */') + '\n'; @@ -487,7 +445,7 @@ function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotR hadDefault = true; s += '' + 'default:\n' + - createStatements(rng(3) + 1, recurmax, canThrow, CAN_BREAK, canContinue, cannotReturn, stmtDepth, inGlobal) + + createStatements(rng(3) + 1, recurmax, canThrow, CAN_BREAK, canContinue, cannotReturn, stmtDepth) + '\n'; } } @@ -508,79 +466,79 @@ function createExpression(recurmax, noComma, stmtDepth, canThrow) { } function _createExpression(recurmax, noComma, stmtDepth, canThrow) { switch (rng(13)) { - 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, NOT_GLOBAL); // 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, NOT_GLOBAL) + '})()'; - break; - case 1: - s = '+function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, NOT_GLOBAL) + '}()'; - break; - case 2: - s = '!function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, NOT_GLOBAL) + '}()'; - break; - default: - s = 'void function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, NOT_GLOBAL) + '}()'; - break; - } - VAR_NAMES.length = nameLenBefore; - return s; - case 9: - return createTypeofExpr(); - 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 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(); + 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); } } @@ -604,16 +562,16 @@ function _createSimpleBinaryExpr(recurmax, noComma) { function createTypeofExpr() { switch (rng(5)) { - case 0: - return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; - case 1: - return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE) + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; - case 2: - return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE) + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; - case 3: - return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; - case 4: - return 'typeof ' + createVarName(MANDATORY, NOT_GLOBAL, DONT_STORE); + 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); } } @@ -634,55 +592,67 @@ function createUnaryOp() { return UNARY_OPS[rng(UNARY_OPS.length)]; } -function createVarName(maybe, inGlobal, dontStore) { +function createVarName(maybe, dontStore) { if (!maybe || rng(2) === 1) { - do { - var r = rng(VAR_NAMES.length); - var suffixed = rng(5) > 0; - var name = VAR_NAMES[r] + (suffixed ? '_' + (++loops) : ''); - } while (inGlobal && (name === 'NaN' || name === 'undefined' || name === 'Infinity')); // prevent nodejs module strict mode problems + 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) { +function try_beautify(code, result) { + try { + var beautified = minify(code, { + fromString: true, + compress: false, + mangle: false, + output: { + beautify: true, + bracketize: true, + }, + }).code; + 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); + } + console.log("//"); + console.log(code); +} + +function log() { 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"); - console.log("//"); - console.log(original_code); + try_beautify(original_code, original_result); console.log(); console.log(); - if (enable_beautifier) { - 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); - if (enable_beautifier) { - console.log("beautified result:"); - console.log(beautify_result); + 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("uglified result:"); - console.log(uglify_result); if (!ok) console.log("!!!!!! Failed... round", round); } for (var round = 0; round < num_iterations; round++) { - var parse_error = false; process.stdout.write(round + " of " + num_iterations + "\r"); VAR_NAMES.length = INITIAL_NAMES_LEN; // prune any previous names still in the list @@ -691,51 +661,25 @@ for (var round = 0; round < num_iterations; round++) { var original_code = [ "var a = 100, b = 10, c = 0;", - createTopLevelCodes(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1) + - "console.log(null, a, b, c);" // the array makes for a cleaner output (empty string still shows up etc) + createTopLevelCodes(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1), + "console.log(null, a, b, c);" // preceding `null` makes for a cleaner output (empty string still shows up etc) ].join("\n"); - var original_result = run_code(original_code); - - if (enable_beautifier) { - try { - var beautify_code = minify(original_code, { - fromString: true, - mangle: false, - compress: false, - output: { - beautify: true, - bracketize: true, - }, - }).code; - } catch(e) { - parse_error = 1; - } - var beautify_result = run_code(beautify_code); - } else { - var beautify_result = original_result; - } + var uglify_code; try { - var uglify_code = minify(original_code, { - fromString: true, - mangle: true, - compress: { - passes: 3, - }, - output: { - //beautify: true, - //bracketize: true, - }, - }).code; - } catch(e) { - parse_error = 2; + uglify_code = minify(original_code, { + fromString: true, + }).code; + } catch (e) { + uglify_code = e; } - var uglify_result = run_code(uglify_code); - var ok = !parse_error && original_result == beautify_result && original_result == uglify_result; - //if (!ok && typeof original_result === 'string' && original_result.indexOf('[Function:') >= 0) ok = true; - 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; + var ok = typeof uglify_code == "string"; + if (ok) { + var original_result = sandbox.run_code(original_code); + var uglify_result = sandbox.run_code(uglify_code); + ok = sandbox.same_stdout(original_result, uglify_result); + } + if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(); + if (!ok) process.exit(1); } From 87f6e1b09146607a2bf1eaa080a645277c767dda Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 1 Apr 2017 17:09:52 +0800 Subject: [PATCH 08/17] minor tweaks to fuzzer (#1751) - remove `let` as variable name - employ `crypto.randomBytes()` --- test/ufuzz.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index 4889598b..c011ea36 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -13,6 +13,7 @@ }); var minify = require("..").minify; +var randomBytes = require("crypto").randomBytes; var sandbox = require("./sandbox"); var MAX_GENERATED_TOPLEVELS_PER_RUN = 3; @@ -252,8 +253,7 @@ var VAR_NAMES = [ 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', - 'Object', - 'let' ]; // maybe omit this, it's more a parser problem than minifier + 'Object']; var INITIAL_NAMES_LEN = VAR_NAMES.length; var TYPEOF_OUTCOMES = [ @@ -271,7 +271,8 @@ var loops = 0; var funcs = 0; function rng(max) { - return Math.floor(max * Math.random()); + var r = parseInt(randomBytes(4).toString("hex"), 16) / 0xFFFFFFFF; + return Math.floor(max * r); } function createTopLevelCodes(n) { From ee3fe0f4cd977213701579565ce68fc6a85916c3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 1 Apr 2017 17:19:57 +0800 Subject: [PATCH 09/17] fix switch branch elimination (#1752) Merge unreachable case body with previous fallthrough case fixes #1750 --- lib/compress.js | 49 ++++++++++++++++++------------------- test/compress/issue-1750.js | 25 +++++++++++++++++++ 2 files changed, 49 insertions(+), 25 deletions(-) create mode 100644 test/compress/issue-1750.js diff --git a/lib/compress.js b/lib/compress.js index 1d9cd401..fc11840d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2537,49 +2537,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); } } 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); 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++]); if (body.length > 0) { body[0].body = decl.concat(body[0].body); } @@ -2612,6 +2602,15 @@ merge(Compressor.prototype, { if (!has_break) return make_node(AST_BlockStatement, self, body[0]).optimize(compressor); } return self; + + function eliminate_branch(branch, prev) { + if (!prev) prev = body[body.length - 1]; + 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){ diff --git a/test/compress/issue-1750.js b/test/compress/issue-1750.js new file mode 100644 index 00000000..53a78e65 --- /dev/null +++ b/test/compress/issue-1750.js @@ -0,0 +1,25 @@ +case_1: { + options = { + dead_code: true, + evaluate: 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" +} From 9a311705f53648388b8e6bd94c73f39aa15effeb Mon Sep 17 00:00:00 2001 From: kzc Date: Sat, 1 Apr 2017 14:08:46 -0400 Subject: [PATCH 10/17] fuzz regexp literals, more constant numbers, typeof expression (#1755) --- test/ufuzz.js | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index c011ea36..f0c0ea67 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -136,14 +136,24 @@ for (var i = 2; i < process.argv.length; ++i) { var VALUES = [ 'true', 'false', - '22', + ' /[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()', - '(-1)', 'NaN', 'undefined', 'Infinity', @@ -153,7 +163,12 @@ var VALUES = [ '([,0].length === 2)', // an array with elisions... this is always true '({})', // wrapped the object causes too many syntax errors in statements '"foo"', - '"bar"' ]; + '"bar"', + '"undefined"', + '"object"', + '"number"', + '"function"', +]; var BINARY_OPS_NO_COMMA = [ ' + ', // spaces needed to disambiguate with ++ cases (could otherwise cause syntax errors) @@ -257,6 +272,7 @@ var VAR_NAMES = [ var INITIAL_NAMES_LEN = VAR_NAMES.length; var TYPEOF_OUTCOMES = [ + 'function', 'undefined', 'string', 'number', @@ -466,7 +482,7 @@ function createExpression(recurmax, noComma, stmtDepth, canThrow) { return _createExpression(recurmax, noComma, stmtDepth, canThrow); } function _createExpression(recurmax, noComma, stmtDepth, canThrow) { - switch (rng(13)) { + switch (rng(15)) { case 0: return createUnaryOp() + (rng(2) === 1 ? 'a' : 'b'); case 1: @@ -540,6 +556,10 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { } 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()) "; } } @@ -562,7 +582,7 @@ function _createSimpleBinaryExpr(recurmax, noComma) { } function createTypeofExpr() { - switch (rng(5)) { + switch (rng(8)) { case 0: return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; case 1: @@ -573,6 +593,10 @@ function createTypeofExpr() { return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; case 4: return 'typeof ' + createVarName(MANDATORY, DONT_STORE); + case 5: + case 6: + case 7: + return '(typeof ' + createExpression(3, COMMA_OK, 2, true) + ')'; } } From 28ecea50a6ae0cc8c5bddca7764e5fd198a7730e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 2 Apr 2017 02:10:50 +0800 Subject: [PATCH 11/17] upgrade fuzzer (#1754) - configurable set of `minify()` options - test and report suspects upon failure - continue after failure if infinite iterations is specified --- test/ufuzz.js | 103 +++++++++++++++++++++++++++++++++++++----------- test/ufuzz.json | 34 ++++++++++++++++ 2 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 test/ufuzz.json diff --git a/test/ufuzz.js b/test/ufuzz.js index f0c0ea67..241e0439 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -12,7 +12,7 @@ stream._handle.setBlocking(true); }); -var minify = require("..").minify; +var UglifyJS = require(".."); var randomBytes = require("crypto").randomBytes; var sandbox = require("./sandbox"); @@ -116,7 +116,6 @@ for (var i = 2; i < process.argv.length; ++i) { console.log(': generate this many cases (if used must be first arg)'); console.log('-v: print every generated test case'); console.log('-V: print every 100th generated test case'); - console.log('-b: also run beautifier'); 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)'); @@ -630,7 +629,7 @@ function createVarName(maybe, dontStore) { function try_beautify(code, result) { try { - var beautified = minify(code, { + var beautified = UglifyJS.minify(code, { fromString: true, compress: false, mangle: false, @@ -652,7 +651,55 @@ function try_beautify(code, result) { console.log(code); } -function log() { +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); @@ -674,9 +721,22 @@ function log() { console.log("// !!! uglify failed !!!"); console.log(uglify_code.stack); } - if (!ok) console.log("!!!!!! Failed... round", round); + 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 = 0; round < num_iterations; round++) { process.stdout.write(round + " of " + num_iterations + "\r"); @@ -684,27 +744,26 @@ for (var round = 0; round < num_iterations; round++) { loops = 0; funcs = 0; - var original_code = [ + original_code = [ "var a = 100, b = 10, c = 0;", createTopLevelCodes(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1), "console.log(null, a, b, c);" // preceding `null` makes for a cleaner output (empty string still shows up etc) ].join("\n"); - var uglify_code; - try { - uglify_code = minify(original_code, { - fromString: true, - }).code; - } catch (e) { - uglify_code = e; - } + minify_options.forEach(function(options) { + try { + uglify_code = UglifyJS.minify(original_code, JSON.parse(options)).code; + } catch (e) { + uglify_code = e; + } - var ok = typeof uglify_code == "string"; - if (ok) { - var original_result = sandbox.run_code(original_code); - var uglify_result = sandbox.run_code(uglify_code); - ok = sandbox.same_stdout(original_result, uglify_result); - } - if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(); - if (!ok) process.exit(1); + 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..8db03d2d --- /dev/null +++ b/test/ufuzz.json @@ -0,0 +1,34 @@ +[ + { + "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, + "unsafe": true, + "warnings": false + } + } +] From 4a55bb0be50a1faea3a752fdf708765d2419a8c4 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 2 Apr 2017 03:17:01 +0800 Subject: [PATCH 12/17] minor tweaks to `test/ufuzz.js` (#1756) - count iterations from `1` instead of `0` - remove `unsafe` from default set of `minify()` tests - improve usability of help --- test/ufuzz.js | 6 ++++-- test/ufuzz.json | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index 241e0439..bd110c3e 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -110,6 +110,8 @@ for (var i = 2; i < process.argv.length; ++i) { 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):'); @@ -128,7 +130,7 @@ for (var i = 2; i < process.argv.length; ++i) { 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 -? for help'); + if (i > 2 || !parseInt(process.argv[i], 10)) throw new Error('Unknown argument[' + process.argv[i] + ']; see -h for help'); } } @@ -737,7 +739,7 @@ var minify_options = require("./ufuzz.json").map(function(options) { }); var original_code, original_result; var uglify_code, uglify_result, ok; -for (var round = 0; round < num_iterations; round++) { +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 diff --git a/test/ufuzz.json b/test/ufuzz.json index 8db03d2d..2d871e87 100644 --- a/test/ufuzz.json +++ b/test/ufuzz.json @@ -27,7 +27,6 @@ "keep_fargs": false, "passes": 3, "pure_getters": true, - "unsafe": true, "warnings": false } } From c076e7b60d356def1b94e5856034b38fc93312fd Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 2 Apr 2017 05:11:29 +0800 Subject: [PATCH 13/17] speed up fuzzer code generation (#1757) - only output one top-level function or statement block - reduce `rng()` granularity from 2^32 to 65536 - fix overflow in `rng()` - track `canThrow` during `typeof` creation --- test/ufuzz.js | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index bd110c3e..7aab6715 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -16,7 +16,7 @@ var UglifyJS = require(".."); var randomBytes = require("crypto").randomBytes; var sandbox = require("./sandbox"); -var MAX_GENERATED_TOPLEVELS_PER_RUN = 3; +var MAX_GENERATED_TOPLEVELS_PER_RUN = 1; var MAX_GENERATION_RECURSION_DEPTH = 12; var INTERVAL_COUNT = 100; @@ -288,22 +288,13 @@ var loops = 0; var funcs = 0; function rng(max) { - var r = parseInt(randomBytes(4).toString("hex"), 16) / 0xFFFFFFFF; + var r = randomBytes(2).readUInt16LE(0) / 65536; return Math.floor(max * r); } -function createTopLevelCodes(n) { - var s = ''; - while (n-- > 0) { - s += createTopLevelCode() + '\n\n//$$$$$$$$$$$$$$\n\n'; - } - return s; -} - function createTopLevelCode() { - var r = rng(3); - if (r > 0) return createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, IN_GLOBAL, ANY_TYPE, CANNOT_THROW, 0); - return createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0); + 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) { @@ -323,7 +314,7 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { 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) === 1) { + 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 { @@ -523,7 +514,7 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { VAR_NAMES.length = nameLenBefore; return s; case 9: - return createTypeofExpr(); + 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... @@ -572,7 +563,7 @@ 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) + ')' + 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` @@ -582,7 +573,7 @@ function _createSimpleBinaryExpr(recurmax, noComma) { return s; } -function createTypeofExpr() { +function createTypeofExpr(recurmax, stmtDepth, canThrow) { switch (rng(8)) { case 0: return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; @@ -594,10 +585,8 @@ function createTypeofExpr() { return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; case 4: return 'typeof ' + createVarName(MANDATORY, DONT_STORE); - case 5: - case 6: - case 7: - return '(typeof ' + createExpression(3, COMMA_OK, 2, true) + ')'; + default: + return '(typeof ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; } } @@ -748,7 +737,7 @@ for (var round = 1; round <= num_iterations; round++) { original_code = [ "var a = 100, b = 10, c = 0;", - createTopLevelCodes(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1), + createTopLevelCode(), "console.log(null, a, b, c);" // preceding `null` makes for a cleaner output (empty string still shows up etc) ].join("\n"); From f7ca4f229795f87674d32e2df3de3cf1f8367a39 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 2 Apr 2017 14:52:25 +0800 Subject: [PATCH 14/17] fix corner cases in switch and undefined (#1762) - fix side effects in switch condition for singular blocks - fix `undefined` confusion with local variable - gate `OPT(AST_Switch)` with `switches` fixes #1758 fixes #1759 --- lib/compress.js | 16 ++++- test/compress/issue-1750.js | 1 + test/compress/reduce_vars.js | 12 ++++ test/compress/sequences.js | 26 ++++++++ test/compress/switch.js | 119 +++++++++++++++++++++++++++++++---- 5 files changed, 159 insertions(+), 15 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index fc11840d..5776fb88 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -77,6 +77,7 @@ function Compressor(options, 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, @@ -1054,7 +1055,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); @@ -2526,6 +2527,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) { @@ -2599,7 +2601,15 @@ 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; @@ -2904,7 +2914,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); } } } diff --git a/test/compress/issue-1750.js b/test/compress/issue-1750.js index 53a78e65..c1448afe 100644 --- a/test/compress/issue-1750.js +++ b/test/compress/issue-1750.js @@ -2,6 +2,7 @@ case_1: { options = { dead_code: true, evaluate: true, + switches: true, } input: { var a = 0, b = 1; 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" +} From d57527697fba37bfad50ca0283326a458cdea031 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 2 Apr 2017 16:14:09 +0800 Subject: [PATCH 15/17] avoid confusion of `NaN` & `Infinity` with `catch` symbol of the same name (#1763) fixes #1760 fixes #1761 --- lib/compress.js | 22 +++++++++++----- test/compress/evaluate.js | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 5776fb88..763490a7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -409,6 +409,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) { @@ -3517,12 +3529,11 @@ 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; @@ -3538,8 +3549,7 @@ merge(Compressor.prototype, { }); OPT(AST_Infinity, function(self, compressor){ - var retain = compressor.option("keep_infinity") - && !compressor.find_parent(AST_Scope).find_variable("Infinity"); + 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, { @@ -3552,7 +3562,7 @@ merge(Compressor.prototype, { }); OPT(AST_NaN, function(self, compressor){ - return compressor.find_parent(AST_Scope).find_variable("NaN") ? make_node(AST_Binary, self, { + return find_variable(compressor, "NaN") ? make_node(AST_Binary, self, { operator: "/", left: make_node(AST_Number, self, { value: 0 diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 0d26e749..fa432c46 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -802,3 +802,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" +} From 9469c03ac976b3cfc1e52abf034ce76f1d6aca59 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 2 Apr 2017 17:07:20 +0800 Subject: [PATCH 16/17] fix corner case in `switch` (#1765) --- lib/compress.js | 7 +++---- test/compress/issue-1750.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 763490a7..79e7d4d2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2557,7 +2557,7 @@ merge(Compressor.prototype, { if (!default_branch) { default_branch = branch; } else { - eliminate_branch(branch); + eliminate_branch(branch, body[body.length - 1]); } } else if (value !== self.expression) { var exp = branch.expression.evaluate(compressor); @@ -2570,7 +2570,7 @@ merge(Compressor.prototype, { default_branch = null; } } else if (exp !== branch.expression) { - eliminate_branch(branch); + eliminate_branch(branch, body[body.length - 1]); continue; } } @@ -2583,7 +2583,7 @@ merge(Compressor.prototype, { } body.push(branch); } - while (i < len) eliminate_branch(self.body[i++]); + while (i < len) eliminate_branch(self.body[i++], body[body.length - 1]); if (body.length > 0) { body[0].body = decl.concat(body[0].body); } @@ -2626,7 +2626,6 @@ merge(Compressor.prototype, { return self; function eliminate_branch(branch, prev) { - if (!prev) prev = body[body.length - 1]; if (prev && !aborts(prev)) { prev.body = prev.body.concat(branch.body); } else { diff --git a/test/compress/issue-1750.js b/test/compress/issue-1750.js index c1448afe..d18bc49f 100644 --- a/test/compress/issue-1750.js +++ b/test/compress/issue-1750.js @@ -24,3 +24,31 @@ case_1: { } 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" +} From b7f6b73f4e4cd49da9a08afb3ce8b59c6b0c92b9 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 2 Apr 2017 17:07:55 +0800 Subject: [PATCH 17/17] v2.8.21 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ffac7c67..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.20", + "version": "2.8.21", "engines": { "node": ">=0.8.0" },