Files
UglifyJS/test/ufuzz/index.js
Alex Lam S.L c7a3e09407 enhance loops & unused (#4074)
- extend `ufuzz` generation of for-in loops
2020-08-26 09:32:55 +08:00

1328 lines
48 KiB
JavaScript

// 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
require("../../tools/exit");
var UglifyJS = require("../..");
var randomBytes = require("crypto").randomBytes;
var sandbox = require("../sandbox");
var reduce_test = require("../reduce");
var MAX_GENERATED_TOPLEVELS_PER_RUN = 1;
var MAX_GENERATION_RECURSION_DEPTH = 12;
var INTERVAL_COUNT = 100;
var STMT_ARG_TO_ID = Object.create(null);
var STMTS_TO_USE = [];
function STMT_(name) {
return STMT_ARG_TO_ID[name] = STMTS_TO_USE.push(STMTS_TO_USE.length) - 1;
}
var STMT_BLOCK = STMT_("block");
var STMT_IF_ELSE = STMT_("ifelse");
var STMT_DO_WHILE = STMT_("dowhile");
var STMT_WHILE = STMT_("while");
var STMT_FOR_LOOP = STMT_("forloop");
var STMT_FOR_IN = STMT_("forin");
var STMT_SEMI = STMT_("semi");
var STMT_EXPR = STMT_("expr");
var STMT_SWITCH = STMT_("switch");
var STMT_VAR = STMT_("var");
var STMT_RETURN_ETC = STMT_("stop");
var STMT_FUNC_EXPR = STMT_("funcexpr");
var STMT_TRY = STMT_("try");
var STMT_C = STMT_("c");
var STMT_FIRST_LEVEL_OVERRIDE = -1;
var STMT_SECOND_LEVEL_OVERRIDE = -1;
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 use_strict = false;
var catch_redef = require.main === module;
var generate_directive = require.main === module;
for (var i = 2; i < process.argv.length; ++i) {
switch (process.argv[i]) {
case "-v":
verbose = true;
break;
case "-V":
verbose_interval = true;
break;
case "-t":
MAX_GENERATED_TOPLEVELS_PER_RUN = +process.argv[++i];
if (!MAX_GENERATED_TOPLEVELS_PER_RUN) throw new Error("Must generate at least one toplevel per run");
break;
case "-r":
MAX_GENERATION_RECURSION_DEPTH = +process.argv[++i];
if (!MAX_GENERATION_RECURSION_DEPTH) throw new Error("Recursion depth must be at least 1");
break;
case "-s1":
var name = process.argv[++i];
STMT_FIRST_LEVEL_OVERRIDE = STMT_ARG_TO_ID[name];
if (!(STMT_FIRST_LEVEL_OVERRIDE >= 0)) throw new Error("Unknown statement name; use -? to get a list");
break;
case "-s2":
var name = process.argv[++i];
STMT_SECOND_LEVEL_OVERRIDE = STMT_ARG_TO_ID[name];
if (!(STMT_SECOND_LEVEL_OVERRIDE >= 0)) throw new Error("Unknown statement name; use -? to get a list");
break;
case "--no-catch-redef":
catch_redef = false;
break;
case "--no-directive":
generate_directive = false;
break;
case "--use-strict":
use_strict = true;
break;
case "--stmt-depth-from-func":
STMT_COUNT_FROM_GLOBAL = false;
break;
case "--only-stmt":
STMTS_TO_USE = process.argv[++i].split(",").map(function(name) {
return STMT_ARG_TO_ID[name];
});
break;
case "--without-stmt":
// meh. it runs once it's fine.
process.argv[++i].split(",").forEach(function(name) {
var omit = STMT_ARG_TO_ID[name];
STMTS_TO_USE = STMTS_TO_USE.filter(function(id) {
return id !== omit;
});
});
break;
case "--help":
case "-h":
case "-?":
println("** UglifyJS fuzzer help **");
println("Valid options (optional):");
println("<number>: generate this many cases (if used must be first arg)");
println("-v: print every generated test case");
println("-V: print every 100th generated test case");
println("-t <int>: generate this many toplevels per run (more take longer)");
println("-r <int>: maximum recursion depth for generator (higher takes longer)");
println("-s1 <statement name>: force the first level statement to be this one (see list below)");
println("-s2 <statement name>: force the second level statement to be this one (see list below)");
println("--no-catch-redef: do not redefine catch variables");
println("--no-directive: do not generate directives");
println('--use-strict: generate "use strict"');
println("--stmt-depth-from-func: reset statement depth counter at each function, counts from global otherwise");
println("--only-stmt <statement names>: a comma delimited white list of statements that may be generated");
println("--without-stmt <statement names>: a comma delimited black list of statements never to generate");
println("List of accepted statement names: " + Object.keys(STMT_ARG_TO_ID));
println("** UglifyJS fuzzer exiting **");
return 0;
default:
// first arg may be a number.
if (i > 2 || !parseInt(process.argv[i], 10)) throw new Error("Unknown argument[" + process.argv[i] + "]; see -h for help");
}
}
var VALUES = [
'"a"',
'"b"',
'"c"',
'""',
"true",
"false",
" /[a2][^e]+$/ ",
"(-1)",
"(-2)",
"(-3)",
"(-4)",
"(-5)",
"0",
"1",
"2",
"3",
"4",
"5",
"22",
"-0", // 0/-0 !== 0
"23..toString()",
"24 .toString()",
"25. ",
"0x26.toString()",
"NaN",
"undefined",
"Infinity",
"null",
"[]",
"[,0][1]", // an array with elisions... but this is always false
"([,0].length === 2)", // an array with elisions... this is always true
"({})", // wrapped the object causes too many syntax errors in statements
'"foo"',
'"bar"',
'"undefined"',
'"object"',
'"number"',
'"function"',
"this",
];
var BINARY_OPS = [
" + ", // spaces needed to disambiguate with ++ cases (could otherwise cause syntax errors)
" - ",
"/",
"*",
"&",
"|",
"^",
"<",
"<=",
">",
">=",
"==",
"===",
"!=",
"!==",
"<<",
">>",
">>>",
"%",
"&&",
"||",
"^",
",",
];
BINARY_OPS = BINARY_OPS.concat(BINARY_OPS);
BINARY_OPS = BINARY_OPS.concat(BINARY_OPS);
BINARY_OPS = BINARY_OPS.concat(BINARY_OPS);
BINARY_OPS = BINARY_OPS.concat(BINARY_OPS);
BINARY_OPS = BINARY_OPS.concat(BINARY_OPS);
BINARY_OPS.push(" in ");
var ASSIGNMENTS = [
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"=",
"+=",
"+=",
"+=",
"+=",
"+=",
"+=",
"+=",
"+=",
"+=",
"+=",
"-=",
"*=",
"/=",
"&=",
"|=",
"^=",
"<<=",
">>=",
">>>=",
"%=",
];
var UNARY_SAFE = [
"+",
"-",
"~",
"!",
"void ",
"delete ",
];
var UNARY_POSTFIX = [
"++",
"--",
];
var UNARY_PREFIX = UNARY_POSTFIX.concat(UNARY_SAFE);
var NO_COMMA = true;
var COMMA_OK = false;
var MAYBE = 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 NO_DEFUN = false;
var DEFUN_OK = true;
var DONT_STORE = true;
var VAR_NAMES = [
"a",
"a",
"a",
"a",
"b",
"b",
"b",
"b",
"c", // prevent redeclaring this, avoid assigning to this
"foo",
"foo",
"bar",
"bar",
"undefined",
"NaN",
"Infinity",
"arguments",
"Math",
"parseInt",
];
var INITIAL_NAMES_LEN = VAR_NAMES.length;
var TYPEOF_OUTCOMES = [
"function",
"undefined",
"string",
"number",
"object",
"boolean",
"special",
"unknown",
"symbol",
"crap",
];
var unique_vars = [];
var loops = 0;
var funcs = 0;
var called = Object.create(null);
var labels = 10000;
function rng(max) {
var r = randomBytes(2).readUInt16LE(0) / 65536;
return Math.floor(max * r);
}
function strictMode() {
return use_strict && rng(4) == 0 ? '"use strict";' : "";
}
function createTopLevelCode() {
VAR_NAMES.length = INITIAL_NAMES_LEN; // prune any previous names still in the list
unique_vars.length = 0;
loops = 0;
funcs = 0;
called = Object.create(null);
return [
strictMode(),
"var _calls_ = 10, a = 100, b = 10, c = 0;",
rng(2) == 0
? createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0)
: createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, DEFUN_OK, CANNOT_THROW, 0),
// preceding `null` makes for a cleaner output (empty string still shows up etc)
"console.log(null, a, b, c, Infinity, NaN, undefined);"
].join("\n");
}
function createFunctions(n, recurmax, allowDefun, canThrow, stmtDepth) {
if (--recurmax < 0) { return ";"; }
var s = "";
while (n-- > 0) {
s += createFunction(recurmax, allowDefun, canThrow, stmtDepth) + "\n";
}
return s;
}
function createParams() {
var params = [];
for (var n = rng(4); --n >= 0;) {
params.push(createVarName(MANDATORY));
}
return params.join(", ");
}
function createArgs(recurmax, stmtDepth, canThrow) {
var args = [];
for (var n = rng(4); --n >= 0;) {
args.push(rng(2) ? createValue() : createExpression(recurmax - 1, COMMA_OK, stmtDepth, canThrow));
}
return args.join(", ");
}
function filterDirective(s) {
if (!generate_directive && !s[1] && /\("/.test(s[2])) s[2] = ";" + s[2];
return s;
}
function createFunction(recurmax, allowDefun, canThrow, stmtDepth) {
if (--recurmax < 0) { return ";"; }
if (!STMT_COUNT_FROM_GLOBAL) stmtDepth = 0;
var namesLenBefore = VAR_NAMES.length;
var name;
if (allowDefun || rng(5) > 0) {
name = "f" + funcs++;
} else {
unique_vars.push("a", "b", "c");
name = createVarName(MANDATORY, !allowDefun);
unique_vars.length -= 3;
}
var s = [
"function " + name + "(" + createParams() + "){",
strictMode()
];
if (rng(5) === 0) {
// functions with functions. lower the recursion to prevent a mess.
s.push(createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), DEFUN_OK, canThrow, stmtDepth));
} else {
// functions with statements
s.push(createStatements(3, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth));
}
s.push("}", "");
s = filterDirective(s).join("\n");
VAR_NAMES.length = namesLenBefore;
if (!allowDefun) {
// avoid "function statements" (decl inside statements)
s = "var " + createVarName(MANDATORY) + " = " + s;
s += "(" + createArgs(recurmax, stmtDepth, canThrow) + ")";
} else if (!(name in called) || rng(3) > 0) {
s += "var " + createVarName(MANDATORY) + " = " + name;
s += "(" + createArgs(recurmax, stmtDepth, canThrow) + ")";
}
return s + ";";
}
function createStatements(n, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) {
if (--recurmax < 0) { return ";"; }
var s = "";
while (--n > 0) {
s += createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + "\n";
}
return s;
}
function enableLoopControl(flag, defaultValue) {
return Array.isArray(flag) && flag.indexOf("") < 0 ? flag.concat("") : flag || defaultValue;
}
function createLabel(canBreak, canContinue) {
var label;
if (rng(10) < 3) {
label = ++labels;
if (Array.isArray(canBreak)) {
canBreak = canBreak.slice();
} else {
canBreak = canBreak ? [ "" ] : [];
}
canBreak.push(label);
if (Array.isArray(canContinue)) {
canContinue = canContinue.slice();
} else {
canContinue = canContinue ? [ "" ] : [];
}
canContinue.push(label);
}
return {
break: canBreak,
continue: canContinue,
target: label ? "L" + label + ": " : ""
};
}
function getLabel(label) {
if (!Array.isArray(label)) return "";
label = label[rng(label.length)];
return label && " L" + label;
}
function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth, target) {
++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
if (target === undefined) {
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:
var label = createLabel(canBreak);
return label.target + "{" + createStatements(rng(5) + 1, recurmax, canThrow, label.break, canContinue, cannotReturn, stmtDepth) + "}";
case STMT_IF_ELSE:
return "if (" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ")" + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + (rng(2) === 1 ? " else " + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) : "");
case STMT_DO_WHILE:
var label = createLabel(canBreak, canContinue);
canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK);
canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE);
return "{var brake" + loop + " = 5; " + label.target + "do {" + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + "} while ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") && --brake" + loop + " > 0);}";
case STMT_WHILE:
var label = createLabel(canBreak, canContinue);
canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK);
canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE);
return "{var brake" + loop + " = 5; " + label.target + "while ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") && --brake" + loop + " > 0)" + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + "}";
case STMT_FOR_LOOP:
var label = createLabel(canBreak, canContinue);
canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK);
canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE);
return label.target + "for (var brake" + loop + " = 5; (" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") && brake" + loop + " > 0; --brake" + loop + ")" + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth);
case STMT_FOR_IN:
var label = createLabel(canBreak, canContinue);
canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK);
canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE);
var key = rng(10) ? "key" + loop : getVarName();
return [
"{var expr" + loop + " = " + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "; ",
label.target + " for (",
/^key/.test(key) ? "var " : "",
key + " in expr" + loop + ") {",
rng(5) > 1 ? "c = 1 + c; var " + createVarName(MANDATORY) + " = expr" + loop + "[" + key + "]; " : "",
createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth),
"}}",
].join("");
case STMT_SEMI:
return use_strict && rng(20) === 0 ? '"use strict";' : ";";
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:
unique_vars.push("c");
var name = createVarName(MANDATORY);
unique_vars.pop();
return "var " + name + ";";
case 1:
// initializer can only have one expression
unique_vars.push("c");
var name = createVarName(MANDATORY);
unique_vars.pop();
return "var " + name + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";";
default:
// initializer can only have one expression
unique_vars.push("c");
var n1 = createVarName(MANDATORY);
var n2 = createVarName(MANDATORY);
unique_vars.pop();
return "var " + n1 + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ", " + n2 + " = " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";";
}
case STMT_RETURN_ETC:
switch (rng(8)) {
case 0:
case 1:
case 2:
case 3:
if (canBreak && rng(5) === 0) return "break" + getLabel(canBreak) + ";";
if (canContinue && rng(5) === 0) return "continue" + getLabel(canContinue) + ";";
if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";";
if (rng(3) == 0) return "/*3*/return;";
return "return " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";";
case 4:
// this is actually more like a parser test, but perhaps it hits some dead code elimination traps
// must wrap in curlies to prevent orphaned `else` statement
// note: you can't `throw` without an expression so don't put a `throw` option in this case
if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";";
return "{ /*2*/ return\n" + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + "}";
default:
// must wrap in curlies to prevent orphaned `else` statement
if (canThrow && rng(5) === 0) return "{ throw " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + "}";
if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ";";
return "{ return " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + "}";
}
case STMT_FUNC_EXPR:
// "In non-strict mode code, functions can only be declared at top level, inside a block, or ..."
// (dont both with func decls in `if`; it's only a parser thing because you cant call them without a block)
return "{" + createFunction(recurmax, NO_DEFUN, 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;
if (!catch_redef) unique_vars.push(catchName);
s += " catch (" + catchName + ") { " + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + " }";
// remove catch name
if (!catch_redef) unique_vars.pop();
if (freshCatchName) VAR_NAMES.splice(nameLenBefore, 1);
}
if (n !== 0) s += " finally { " + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + " }";
return s;
case STMT_C:
return "c = c + 1;";
default:
throw "no";
}
}
function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) {
var hadDefault = false;
var s = [""];
canBreak = enableLoopControl(canBreak, CAN_BREAK);
while (n-- > 0) {
//hadDefault = n > 0; // disables weird `default` clause positioning (use when handling destabilizes)
if (hadDefault || rng(5) > 0) {
s.push(
"case " + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ":",
createStatements(rng(3) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth),
rng(10) > 0 ? " break;" : "/* fall-through */",
""
);
} else {
hadDefault = true;
s.push(
"default:",
createStatements(rng(3) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth),
""
);
}
}
return s.join("\n");
}
function createExpression(recurmax, noComma, stmtDepth, canThrow) {
if (--recurmax < 0) {
return "(c = 1 + c, " + createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ")"; // note: should return a simple non-recursing expression value!
}
// since `a` and `b` are our canaries we want them more frequently than other expressions (1/3rd chance of a canary)
switch (rng(6)) {
case 0:
return "(a++ + (" + _createExpression(recurmax, noComma, stmtDepth, canThrow) + "))";
case 1:
return "((--b) + (" + _createExpression(recurmax, noComma, stmtDepth, canThrow) + "))";
case 2:
return "((c = c + 1) + (" + _createExpression(recurmax, noComma, stmtDepth, canThrow) + "))"; // c only gets incremented
default:
return "(" + _createExpression(recurmax, noComma, stmtDepth, canThrow) + ")";
}
}
function _createExpression(recurmax, noComma, stmtDepth, canThrow) {
var p = 0;
switch (rng(_createExpression.N)) {
case p++:
case p++:
return createUnaryPrefix() + (rng(2) === 1 ? "a" : "b");
case p++:
case p++:
return (rng(2) === 1 ? "a" : "b") + createUnaryPostfix();
case p++:
case p++:
// parens needed because assignments aren't valid unless they're the left-most op(s) in an expression
return "b " + createAssignment() + " a";
case p++:
case p++:
return rng(2) + " === 1 ? a : b";
case p++:
case p++:
return createValue();
case p++:
case p++:
return getVarName();
case p++:
return getVarName() + createAssignment() + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow);
case p++:
return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow);
case p++:
return createExpression(recurmax, noComma, stmtDepth, canThrow) + "?" + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ":" + createExpression(recurmax, noComma, stmtDepth, canThrow);
case p++:
case p++:
var nameLenBefore = VAR_NAMES.length;
unique_vars.push("c");
var name = createVarName(MAYBE); // note: this name is only accessible from _within_ the function. and immutable at that.
unique_vars.pop();
var s = [];
switch (rng(5)) {
case 0:
s.push(
"(function " + name + "(){",
strictMode(),
createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth),
rng(2) == 0 ? "})" : "})()"
);
break;
case 1:
s.push(
"+function " + name + "(){",
strictMode(),
createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth),
"}()"
);
break;
case 2:
s.push(
"!function " + name + "(){",
strictMode(),
createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth),
"}()"
);
break;
case 3:
s.push(
"void function " + name + "(){",
strictMode(),
createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth),
"}()"
);
break;
default:
var instantiate = rng(4) ? "new " : "";
s.push(
instantiate + "function " + name + "(){",
strictMode()
);
if (instantiate) for (var i = rng(4); --i >= 0;) {
if (rng(2)) s.push("this." + getDotKey(true) + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ";");
else s.push("this[" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]" + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ";");
}
s.push(
createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth),
rng(2) == 0 ? "}" : "}()"
);
break;
}
VAR_NAMES.length = nameLenBefore;
return filterDirective(s).join("\n");
case p++:
case p++:
return createTypeofExpr(recurmax, stmtDepth, canThrow);
case p++:
case p++:
// more like a parser test but perhaps comment nodes mess up the analysis?
// note: parens not needed for post-fix (since that's the default when ambiguous)
// for prefix ops we need parens to prevent accidental syntax errors.
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 p++:
case p++:
return createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow);
case p++:
case p++:
return createUnarySafePrefix() + "(" + createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ")";
case p++:
return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || a || 3).toString() ";
case p++:
return " /[abc4]/.test(((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || b || 5).toString()) ";
case p++:
return " /[abc4]/g.exec(((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || b || 5).toString()) ";
case p++:
return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) +
") || " + rng(10) + ").toString()[" +
createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "] ";
case p++:
return createArrayLiteral(recurmax, stmtDepth, canThrow);
case p++:
return createObjectLiteral(recurmax, stmtDepth, canThrow);
case p++:
return createArrayLiteral(recurmax, stmtDepth, canThrow) + "[" +
createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]";
case p++:
return createObjectLiteral(recurmax, stmtDepth, canThrow) + "[" +
createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]";
case p++:
return createArrayLiteral(recurmax, stmtDepth, canThrow) + "." + getDotKey();
case p++:
return createObjectLiteral(recurmax, stmtDepth, canThrow) + "." + getDotKey();
case p++:
return createValue() + " in " + createArrayLiteral(recurmax, stmtDepth, canThrow);
case p++:
return createValue() + " in " + createObjectLiteral(recurmax, stmtDepth, canThrow);
case p++:
var name = getVarName();
var s = name + "[" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]";
return canThrow && rng(8) == 0 ? s : name + " && " + s;
case p++:
var name = getVarName();
var s = name + "." + getDotKey();
return canThrow && rng(8) == 0 ? s : name + " && " + s;
case p++:
case p++:
var name = getVarName();
var s = name + "." + getDotKey();
s = "typeof " + s + ' == "function" && --_calls_ >= 0 && ' + s + "(" + createArgs(recurmax, stmtDepth, canThrow) + ")";
return canThrow && rng(8) == 0 ? s : name + " && " + s;
case p++:
case p++:
case p++:
case p++:
var name = rng(3) == 0 ? getVarName() : "f" + rng(funcs + 2);
called[name] = true;
return "typeof " + name + ' == "function" && --_calls_ >= 0 && ' + name + "(" + createArgs(recurmax, stmtDepth, canThrow) + ")";
}
_createExpression.N = p;
return _createExpression(recurmax, noComma, stmtDepth, canThrow);
}
function createArrayLiteral(recurmax, stmtDepth, canThrow) {
recurmax--;
var arr = "[";
for (var i = rng(6); --i >= 0;) {
// in rare cases produce an array hole element
var element = rng(20) ? createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) : "";
arr += element + ", ";
}
return arr + "]";
}
var SAFE_KEYS = [
"length",
"foo",
"a",
"b",
"c",
"undefined",
"null",
"NaN",
"Infinity",
"in",
"var",
];
var KEYS = [
"''",
'"\t"',
'"-2"',
"0",
"1.5",
"3",
].concat(SAFE_KEYS);
function getDotKey(assign) {
var key;
do {
key = SAFE_KEYS[rng(SAFE_KEYS.length)];
} while (assign && key == "length");
return key;
}
function createAccessor(recurmax, stmtDepth, canThrow) {
var namesLenBefore = VAR_NAMES.length;
var s;
var prop1 = getDotKey();
if (rng(2) == 0) {
s = [
"get " + prop1 + "(){",
strictMode(),
createStatements(2, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth),
createStatement(recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth, STMT_RETURN_ETC),
"},"
];
} else {
var prop2;
do {
prop2 = getDotKey();
} while (prop1 == prop2);
s = [
"set " + prop1 + "(" + createVarName(MANDATORY) + "){",
strictMode(),
createStatements(2, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth),
"this." + prop2 + createAssignment() + _createBinaryExpr(recurmax, COMMA_OK, stmtDepth, canThrow) + ";",
"},"
];
}
VAR_NAMES.length = namesLenBefore;
return filterDirective(s).join("\n");
}
function createObjectLiteral(recurmax, stmtDepth, canThrow) {
recurmax--;
var obj = ["({"];
for (var i = rng(6); --i >= 0;) {
if (rng(20) == 0) {
obj.push(createAccessor(recurmax, stmtDepth, canThrow));
} else {
var key = KEYS[rng(KEYS.length)];
obj.push(key + ":(" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "),");
}
}
obj.push("})");
return obj.join("\n");
}
function createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) {
recurmax = 3; // note that this generates 2^recurmax expression parts... make sure to cap it
return _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow);
}
function _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) {
return "(" + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow)
+ createBinaryOp(noComma, canThrow) + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ")";
}
function _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) {
// intentionally generate more hardcore ops
if (--recurmax < 0) return createValue();
var assignee, expr;
switch (rng(30)) {
case 0:
return "(c = c + 1, " + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ")";
case 1:
return "(" + createUnarySafePrefix() + "(" + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + "))";
case 2:
assignee = getVarName();
return "(" + assignee + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ")";
case 3:
assignee = getVarName();
expr = "(" + assignee + "[" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow)
+ "]" + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ")";
return canThrow && rng(10) == 0 ? expr : "(" + assignee + " && " + expr + ")";
case 4:
assignee = getVarName();
expr = "(" + assignee + "." + getDotKey(true) + createAssignment()
+ _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ")";
return canThrow && rng(10) == 0 ? expr : "(" + assignee + " && " + expr + ")";
default:
return _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow);
}
}
function createTypeofExpr(recurmax, stmtDepth, canThrow) {
switch (rng(8)) {
case 0:
return "(typeof " + createVarName(MANDATORY, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")';
case 1:
return "(typeof " + createVarName(MANDATORY, DONT_STORE) + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")';
case 2:
return "(typeof " + createVarName(MANDATORY, DONT_STORE) + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")';
case 3:
return "(typeof " + createVarName(MANDATORY, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")';
case 4:
return "(typeof " + createVarName(MANDATORY, DONT_STORE) + ")";
default:
return "(typeof " + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ")";
}
}
function createValue() {
return VALUES[rng(VALUES.length)];
}
function createBinaryOp(noComma, canThrow) {
var op;
do {
op = BINARY_OPS[rng(BINARY_OPS.length)];
} while (noComma && op == "," || !canThrow && op == " in ");
return op;
}
function createAssignment() {
return ASSIGNMENTS[rng(ASSIGNMENTS.length)];
}
function createUnarySafePrefix() {
return UNARY_SAFE[rng(UNARY_SAFE.length)];
}
function createUnaryPrefix() {
return UNARY_PREFIX[rng(UNARY_PREFIX.length)];
}
function createUnaryPostfix() {
return UNARY_POSTFIX[rng(UNARY_POSTFIX.length)];
}
function getVarName() {
// try to get a generated name reachable from current scope. default to just `a`
return VAR_NAMES[INITIAL_NAMES_LEN + rng(VAR_NAMES.length - INITIAL_NAMES_LEN)] || "a";
}
function createVarName(maybe, dontStore) {
if (!maybe || rng(2)) {
var suffix = rng(3);
var name;
do {
name = VAR_NAMES[rng(VAR_NAMES.length)];
if (suffix) name += "_" + suffix;
} while (unique_vars.indexOf(name) >= 0);
if (suffix && !dontStore) VAR_NAMES.push(name);
return name;
}
return "";
}
if (require.main !== module) {
exports.createTopLevelCode = createTopLevelCode;
exports.num_iterations = num_iterations;
return;
}
function writeln(stream, msg) {
if (typeof msg != "undefined") {
stream.write(typeof msg == "string" ? msg : msg.stack || "" + msg);
}
stream.write("\n");
}
function println(msg) {
writeln(process.stdout, msg);
}
function errorln(msg) {
writeln(process.stderr, msg);
}
function try_beautify(code, toplevel, result, printfn) {
var beautified = UglifyJS.minify(code, {
compress: false,
mangle: false,
output: {
beautify: true,
braces: true,
},
});
if (beautified.error) {
printfn("// !!! beautify failed !!!");
printfn(beautified.error);
} else if (sandbox.same_stdout(sandbox.run_code(beautified.code, toplevel), result)) {
printfn("// (beautified)");
printfn(beautified.code);
return;
}
printfn("//");
printfn(code);
}
var default_options = UglifyJS.default_options();
function log_suspects(minify_options, component) {
var options = component in minify_options ? minify_options[component] : true;
if (!options) return;
if (typeof options != "object") options = {};
var defs = default_options[component];
var toplevel = sandbox.has_toplevel(minify_options);
var suspects = Object.keys(defs).filter(function(name) {
var flip = name == "keep_fargs";
if (flip !== (name in options ? options : defs)[name]) {
var m = JSON.parse(JSON.stringify(minify_options));
var o = JSON.parse(JSON.stringify(options));
o[name] = flip;
m[component] = o;
m.validate = true;
var result = UglifyJS.minify(original_code, m);
if (typeof uglify_code != "string") {
return !sandbox.same_stdout(uglify_code, result.error);
} else if (result.error) {
errorln("Error testing options." + component + "." + name);
errorln(result.error);
} else {
var r = sandbox.run_code(result.code, toplevel);
return !sandbox.same_stdout(uglify_result, r);
}
}
});
if (suspects.length > 0) {
errorln("Suspicious " + component + " options:");
suspects.forEach(function(name) {
errorln(" " + name);
});
errorln();
}
}
function log_suspects_global(options, toplevel) {
var suspects = Object.keys(default_options).filter(function(component) {
return typeof default_options[component] != "object";
}).filter(function(component) {
var m = JSON.parse(options);
m[component] = false;
m.validate = true;
var result = UglifyJS.minify(original_code, m);
if (typeof uglify_code != "string") {
return !sandbox.same_stdout(uglify_code, result.error);
} else if (result.error) {
errorln("Error testing options." + component);
errorln(result.error);
} else {
var r = sandbox.run_code(result.code, toplevel);
return !sandbox.same_stdout(uglify_result, r);
}
});
if (suspects.length > 0) {
errorln("Suspicious options:");
suspects.forEach(function(name) {
errorln(" " + name);
});
errorln();
}
}
function log(options) {
var toplevel = sandbox.has_toplevel(JSON.parse(options));
if (!ok) errorln("\n\n\n\n\n\n!!!!!!!!!!\n\n\n");
errorln("//=============================================================");
if (!ok) errorln("// !!!!!! Failed... round " + round);
errorln("// original code");
var beautified = UglifyJS.minify(original_code, {
compress: false,
mangle: false,
output: {
beautify: true,
braces: true,
},
});
if (beautified.error) {
errorln("// !!! beautify failed !!!");
errorln(beautified.error);
errorln("//");
errorln(original_code);
} else {
var uglified = UglifyJS.minify(beautified.code, JSON.parse(options));
var expected, actual;
if (typeof uglify_code != "string" || uglified.error) {
expected = uglify_code;
actual = uglified.error;
} else {
expected = uglify_result;
actual = sandbox.run_code(uglified.code, toplevel);
}
if (sandbox.same_stdout(expected, actual)) {
errorln("// (beautified)");
errorln(beautified.code);
} else {
errorln("//");
errorln(original_code);
}
}
errorln();
errorln();
errorln("//-------------------------------------------------------------");
if (typeof uglify_code == "string") {
errorln("// uglified code");
try_beautify(uglify_code, toplevel, uglify_result, errorln);
errorln();
errorln();
errorln("original result:");
errorln(original_result);
errorln("uglified result:");
errorln(uglify_result);
} else {
errorln("// !!! uglify failed !!!");
errorln(uglify_code);
if (errored) {
errorln();
errorln();
errorln("original stacktrace:");
errorln(original_result);
}
}
errorln("//-------------------------------------------------------------");
var reduce_options = JSON.parse(options);
reduce_options.validate = true;
var reduced = reduce_test(original_code, reduce_options, {
verbose: false,
}).code;
if (reduced) {
errorln();
errorln("// reduced test case (output will differ)");
errorln();
errorln(reduced);
errorln();
errorln("//-------------------------------------------------------------");
}
errorln("minify(options):");
errorln(JSON.stringify(JSON.parse(options), null, 2));
errorln();
if (!ok) {
Object.keys(default_options).filter(function(component) {
var defs = default_options[component];
return defs && typeof defs == "object";
}).forEach(log_suspects.bind(null, JSON.parse(options)));
log_suspects_global(options, toplevel);
errorln("!!!!!! Failed... round " + round);
}
}
function sort_globals(code) {
var globals = sandbox.run_code("throw Object.keys(this).sort();" + code);
return globals.length ? "var " + globals.join(",") + ";" + code : code;
}
function fuzzy_match(original, uglified) {
uglified = uglified.split(" ");
var i = uglified.length;
original = original.split(" ", i);
while (--i >= 0) {
if (original[i] === uglified[i]) continue;
var a = +original[i];
var b = +uglified[i];
if (Math.abs((b - a) / a) < 1e-10) continue;
return false;
}
return true;
}
function patch_try_catch(orig, toplevel) {
var stack = [ {
code: orig,
index: 0,
offset: 0,
tries: [],
} ];
var re = /(?:(?:^|[\s{}):;])try|}\s*catch\s*\(([^)]+)\)|}\s*finally)\s*(?={)/g;
while (stack.length) {
var code = stack[0].code;
var offset = stack[0].offset;
var tries = stack[0].tries;
var match;
re.lastIndex = stack.shift().index;
while (match = re.exec(code)) {
var index = match.index + match[0].length + 1;
if (/(?:^|[\s{}):;])try\s*$/.test(match[0])) {
tries.unshift({ try: index - offset });
continue;
}
var insert;
if (/}\s*finally\s*$/.test(match[0])) {
tries.shift();
insert = 'if (typeof UFUZZ_ERROR == "object") throw UFUZZ_ERROR;';
} else {
while (tries.length && tries[0].catch) tries.shift();
tries[0].catch = index - offset;
insert = [
"if (!" + match[1] + ".ufuzz_var) {",
match[1] + '.ufuzz_var = "' + match[1] + '";',
match[1] + ".ufuzz_try = " + tries[0].try + ";",
match[1] + ".ufuzz_catch = " + tries[0].catch + ";",
"UFUZZ_ERROR = " + match[1] + ";",
"}",
"throw " + match[1] + ";",
].join("\n");
}
var new_code = code.slice(0, index) + insert + code.slice(index);
var result = sandbox.run_code(new_code, toplevel);
if (typeof result != "object" || typeof result.name != "string" || typeof result.message != "string") {
if (!stack.filled && match[1]) stack.push({
code: code,
index: index && index - 1,
offset: offset,
tries: JSON.parse(JSON.stringify(tries)),
});
offset += insert.length;
code = new_code;
} else if (result.name == "TypeError" && /'in'/.test(result.message)) {
index = result.ufuzz_catch;
return orig.slice(0, index) + result.ufuzz_var + ' = new Error("invalid `in`");' + orig.slice(index);
} else if (result.name == "RangeError" && result.message == "Maximum call stack size exceeded") {
index = result.ufuzz_try;
return orig.slice(0, index) + 'throw new Error("skipping infinite recursion");' + orig.slice(index);
}
}
stack.filled = true;
}
}
var fallback_options = [ JSON.stringify({
compress: false,
mangle: false
}) ];
var minify_options = require("./options.json").map(JSON.stringify);
var original_code, original_result, errored;
var uglify_code, uglify_result, ok;
for (var round = 1; round <= num_iterations; round++) {
process.stdout.write(round + " of " + num_iterations + "\r");
original_code = createTopLevelCode();
var orig_result = [ sandbox.run_code(original_code) ];
errored = typeof orig_result[0] != "string";
if (!errored) orig_result.push(sandbox.run_code(original_code, true));
(errored ? fallback_options : minify_options).forEach(function(options) {
var o = JSON.parse(options);
var toplevel = sandbox.has_toplevel(o);
o.validate = true;
uglify_code = UglifyJS.minify(original_code, o);
original_result = orig_result[toplevel ? 1 : 0];
if (!uglify_code.error) {
uglify_code = uglify_code.code;
uglify_result = sandbox.run_code(uglify_code, toplevel);
ok = sandbox.same_stdout(original_result, uglify_result);
// ignore declaration order of global variables
if (!ok && !toplevel) {
ok = sandbox.same_stdout(sandbox.run_code(sort_globals(original_code)), sandbox.run_code(sort_globals(uglify_code)));
}
// ignore numerical imprecision caused by `unsafe_math`
if (!ok && typeof uglify_result == "string" && o.compress && o.compress.unsafe_math) {
ok = fuzzy_match(original_result, uglify_result);
if (!ok) {
var fuzzy_result = sandbox.run_code(original_code.replace(/( - 0\.1){3}/g, " - 0.3"), toplevel);
ok = sandbox.same_stdout(fuzzy_result, uglify_result);
}
}
// ignore difference in error message caused by `in`
// ignore difference in depth of termination caused by infinite recursion
if (!ok) {
var orig_skipped = patch_try_catch(original_code, toplevel);
var uglify_skipped = patch_try_catch(uglify_code, toplevel);
if (orig_skipped && uglify_skipped) {
ok = sandbox.same_stdout(sandbox.run_code(orig_skipped, toplevel), sandbox.run_code(uglify_skipped, toplevel));
}
}
} else {
uglify_code = uglify_code.error;
ok = errored && uglify_code.name == original_result.name;
}
if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(options);
else if (errored) {
println("//=============================================================");
println("// original code");
try_beautify(original_code, toplevel, original_result, println);
println();
println();
println("original result:");
println(original_result);
println();
}
if (!ok && isFinite(num_iterations)) {
println();
process.exit(1);
}
});
}
println();