diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 31ba69d3..44afb511 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,9 @@ -- Bug report or feature request? +- Bug report or feature request? - `uglify-js` version (`uglifyjs -V`) - JavaScript input - ideally as small as possible. - The `uglifyjs` CLI command executed or `minify()` options used. - An example of JavaScript output produced and/or the error or warning. - -Note: the release version of `uglify-js` only supports ES5. Those wishing to minify ES6 should use the experimental [`harmony`](https://github.com/mishoo/UglifyJS2#harmony) branch. + diff --git a/bin/uglifyjs b/bin/uglifyjs index e39a4b4b..635ca365 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -561,7 +561,7 @@ function getOptions(flag, constants) { var ast; try { - ast = UglifyJS.parse(x, { expression: true }); + ast = UglifyJS.parse(x, { cli: true, expression: true }); } catch(ex) { if (ex instanceof UglifyJS.JS_Parse_Error) { print_error("Error parsing arguments for flag `" + flag + "': " + x); diff --git a/lib/compress.js b/lib/compress.js index a1d2244a..a5397b44 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -261,7 +261,7 @@ merge(Compressor.prototype, { if (node instanceof AST_SymbolRef) { var d = node.definition(); d.references.push(node); - if (!d.fixed || !is_safe(d) + if (d.fixed === undefined || !is_safe(d) || is_modified(node, 0, d.fixed instanceof AST_Lambda)) { d.fixed = false; } @@ -274,10 +274,10 @@ merge(Compressor.prototype, { node.name.walk(suppressor); } else { var d = node.name.definition(); - if (d.fixed === undefined) { - d.fixed = node.value || make_node(AST_Undefined, node); + if (d.fixed == null) { + d.fixed = node.value; mark_as_safe(d); - } else { + } else if (node.value) { d.fixed = false; } } @@ -346,7 +346,7 @@ merge(Compressor.prototype, { pop(); return true; } - if (node instanceof AST_Catch) { + if (node instanceof AST_Catch || node instanceof AST_SwitchBranch) { push(); descend(); pop(); @@ -362,7 +362,14 @@ merge(Compressor.prototype, { function is_safe(def) { for (var i = safe_ids.length, id = def.id; --i >= 0;) { - if (safe_ids[i][id]) return true; + if (safe_ids[i][id]) { + if (def.fixed == null) { + var orig = def.orig[0]; + if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; + def.fixed = make_node(AST_Undefined, orig); + } + return true; + } } } @@ -488,15 +495,6 @@ merge(Compressor.prototype, { return x; }; - var readOnlyPrefix = makePredicate("! ~ + - void typeof"); - function statement_to_expression(stat) { - if (stat.body instanceof AST_UnaryPrefix && readOnlyPrefix(stat.body.operator)) { - return stat.body.expression; - } else { - return stat.body; - } - } - function is_iife_call(node) { if (node instanceof AST_Call && !(node instanceof AST_New)) { return node.expression instanceof AST_Function || is_iife_call(node.expression); @@ -606,9 +604,10 @@ merge(Compressor.prototype, { // Restrict var replacement to constants if side effects encountered. if (side_effects_encountered |= lvalues_encountered) continue; + var value_has_side_effects = var_decl.value.has_side_effects(compressor); // Non-constant single use vars can only be replaced in same scope. if (ref.scope.get_defun_scope() !== self) { - side_effects_encountered |= var_decl.value.has_side_effects(compressor); + side_effects_encountered |= value_has_side_effects; continue; } @@ -635,6 +634,7 @@ merge(Compressor.prototype, { || (parent instanceof AST_If && node !== parent.condition) || (parent instanceof AST_Conditional && node !== parent.condition) || (node instanceof AST_SymbolRef + && value_has_side_effects && !are_references_in_scope(node.definition(), self)) || (parent instanceof AST_Binary && (parent.operator == "&&" || parent.operator == "||") @@ -998,10 +998,10 @@ merge(Compressor.prototype, { }; statements.forEach(function(stat){ if (stat instanceof AST_SimpleStatement) { - if (seqLength(seq) >= compressor.sequences_limit) { - push_seq(); - } - seq.push(seq.length > 0 ? statement_to_expression(stat) : stat.body); + if (seqLength(seq) >= compressor.sequences_limit) push_seq(); + var body = stat.body; + if (seq.length > 0) body = body.drop_side_effect_free(compressor); + if (body) seq.push(body); } else { push_seq(); ret.push(stat); @@ -1050,7 +1050,7 @@ merge(Compressor.prototype, { stat.init = cons_seq(stat.init); } else if (!stat.init) { - stat.init = statement_to_expression(prev); + stat.init = prev.body.drop_side_effect_free(compressor); ret.pop(); } } catch(ex) { @@ -1190,9 +1190,9 @@ merge(Compressor.prototype, { && this.left.is_number(compressor) && this.right.is_number(compressor); }); - var assign = makePredicate("-= *= /= %= &= |= ^= <<= >>= >>>="); def(AST_Assign, function(compressor){ - return assign(this.operator) || this.right.is_number(compressor); + return binary(this.operator.slice(0, -1)) + || this.operator == "=" && this.right.is_number(compressor); }); def(AST_Seq, function(compressor){ return this.cdr.is_number(compressor); @@ -1231,8 +1231,10 @@ merge(Compressor.prototype, { node.DEFMETHOD("is_string", func); }); + var unary_side_effects = makePredicate("delete ++ --"); + function isLHS(node, parent) { - return parent instanceof AST_Unary && (parent.operator == "++" || parent.operator == "--") + return parent instanceof AST_Unary && unary_side_effects(parent.operator) || parent instanceof AST_Assign && parent.left === node; } @@ -1636,14 +1638,37 @@ merge(Compressor.prototype, { return false; }); - def(AST_Block, function(compressor){ - for (var i = this.body.length; --i >= 0;) { - if (this.body[i].has_side_effects(compressor)) + function any(list, compressor) { + for (var i = list.length; --i >= 0;) + if (list[i].has_side_effects(compressor)) return true; - } return false; - }); + } + def(AST_Block, function(compressor){ + return any(this.body, compressor); + }); + def(AST_Switch, function(compressor){ + return this.expression.has_side_effects(compressor) + || any(this.body, compressor); + }); + def(AST_Case, function(compressor){ + return this.expression.has_side_effects(compressor) + || any(this.body, compressor); + }); + def(AST_Try, function(compressor){ + return any(this.body, compressor) + || this.bcatch && this.bcatch.has_side_effects(compressor) + || this.bfinally && this.bfinally.has_side_effects(compressor); + }); + def(AST_If, function(compressor){ + return this.condition.has_side_effects(compressor) + || this.body && this.body.has_side_effects(compressor) + || this.alternative && this.alternative.has_side_effects(compressor); + }); + def(AST_LabeledStatement, function(compressor){ + return this.body.has_side_effects(compressor); + }); def(AST_SimpleStatement, function(compressor){ return this.body.has_side_effects(compressor); }); @@ -1662,17 +1687,14 @@ merge(Compressor.prototype, { || this.alternative.has_side_effects(compressor); }); def(AST_Unary, function(compressor){ - return member(this.operator, ["delete", "++", "--"]) + return unary_side_effects(this.operator) || this.expression.has_side_effects(compressor); }); def(AST_SymbolRef, function(compressor){ return this.global() && this.undeclared(); }); def(AST_Object, function(compressor){ - for (var i = this.properties.length; --i >= 0;) - if (this.properties[i].has_side_effects(compressor)) - return true; - return false; + return any(this.properties, compressor); }); def(AST_ObjectProperty, function(compressor){ if (this.key instanceof AST_ObjectKeyVal && @@ -1681,10 +1703,7 @@ merge(Compressor.prototype, { return this.value.has_side_effects(compressor); }); def(AST_Array, function(compressor){ - for (var i = this.elements.length; --i >= 0;) - if (this.elements[i].has_side_effects(compressor)) - return true; - return false; + return any(this.elements, compressor); }); def(AST_Dot, function(compressor){ if (!compressor.option("pure_getters")) return true; @@ -1927,9 +1946,13 @@ merge(Compressor.prototype, { function before(node, descend, in_list) { if (node instanceof AST_Function && node.name - && !compressor.option("keep_fnames") - && !(node.name.definition().id in in_use_ids)) { - node.name = null; + && !compressor.option("keep_fnames")) { + var def = node.name.definition(); + // any declarations with same name will overshadow + // name of this anonymous function and can therefore + // never be used anywhere + if (!(def.id in in_use_ids) || def.orig.length > 1) + node.name = null; } if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { var trim = !compressor.option("keep_fargs"); @@ -1980,9 +2003,14 @@ merge(Compressor.prototype, { var def = node.definitions.filter(function(def){ if (def.value) def.value = def.value.transform(tt); if (def.name instanceof AST_Destructuring) return true; - if (def.name.definition().id in in_use_ids) return true; - if (!drop_vars && def.name.definition().global) return true; - + var sym = def.name.definition(); + if (sym.id in in_use_ids) return true; + if (!drop_vars && sym.global) return true; + if (sym.orig[0] instanceof AST_SymbolCatch + && sym.scope.parent_scope.find_variable(def.name).orig[0] === def.name) { + def.value = def.value && def.value.drop_side_effect_free(compressor); + return true; + } var w = { name : def.name.name, file : def.name.start.file, @@ -2069,6 +2097,9 @@ merge(Compressor.prototype, { return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { body: body }); + } else if (is_empty(node.init)) { + node.init = null; + return node; } } if (node instanceof AST_BlockStatement) { @@ -2266,6 +2297,7 @@ merge(Compressor.prototype, { switch (this.operator) { case "&&": case "||": + if (right === this.right) return this; var node = this.clone(); node.right = right; return node; @@ -2299,26 +2331,19 @@ merge(Compressor.prototype, { return node; }); def(AST_Unary, function(compressor, first_in_statement){ - switch (this.operator) { - case "delete": - case "++": - case "--": - return this; - case "typeof": - if (this.expression instanceof AST_SymbolRef) return null; - default: - var expression = this.expression.drop_side_effect_free(compressor, first_in_statement); - if (first_in_statement - && this instanceof AST_UnaryPrefix - && is_iife_call(expression)) { - if (expression === this.expression && this.operator.length === 1) return this; - return make_node(AST_UnaryPrefix, this, { - operator: this.operator.length === 1 ? this.operator : "!", - expression: expression - }); - } - return expression; + if (unary_side_effects(this.operator)) return this; + if (this.operator == "typeof" && this.expression instanceof AST_SymbolRef) return null; + var expression = this.expression.drop_side_effect_free(compressor, first_in_statement); + if (first_in_statement + && this instanceof AST_UnaryPrefix + && is_iife_call(expression)) { + if (expression === this.expression && this.operator.length === 1) return this; + return make_node(AST_UnaryPrefix, this, { + operator: this.operator.length === 1 ? this.operator : "!", + expression: expression + }); } + return expression; }); def(AST_SymbolRef, function() { return this.undeclared() ? this : null; @@ -2526,8 +2551,8 @@ merge(Compressor.prototype, { return make_node(AST_SimpleStatement, self, { body: make_node(AST_Conditional, self, { condition : self.condition, - consequent : statement_to_expression(self.body), - alternative : statement_to_expression(self.alternative) + consequent : self.body.body, + alternative : self.alternative.body }) }).optimize(compressor); } @@ -2543,25 +2568,24 @@ merge(Compressor.prototype, { body: make_node(AST_Binary, self, { operator : "||", left : negated, - right : statement_to_expression(self.body) + right : self.body.body }) }).optimize(compressor); return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "&&", left : self.condition, - right : statement_to_expression(self.body) + right : self.body.body }) }).optimize(compressor); } if (self.body instanceof AST_EmptyStatement - && self.alternative && self.alternative instanceof AST_SimpleStatement) { return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "||", left : self.condition, - right : statement_to_expression(self.alternative) + right : self.alternative.body }) }).optimize(compressor); } @@ -2611,105 +2635,93 @@ merge(Compressor.prototype, { }); OPT(AST_Switch, function(self, compressor){ - if (self.body.length == 0 && compressor.option("conditionals")) { - return make_node(AST_SimpleStatement, self, { - body: self.expression - }).transform(compressor); + var branch; + var value = self.expression.evaluate(compressor); + if (value !== self.expression) { + var expression = make_node_from_constant(value, self.expression).transform(compressor); + self.expression = best_of_expression(expression, self.expression); } - for(;;) { - var last_branch = self.body[self.body.length - 1]; - if (last_branch) { - var stat = last_branch.body[last_branch.body.length - 1]; // last statement - if (stat instanceof AST_Break && loop_body(compressor.loopcontrol_target(stat.label)) === self) - last_branch.body.pop(); - if (last_branch instanceof AST_Default && last_branch.body.length == 0) { - self.body.pop(); + if (!compressor.option("dead_code")) return self; + var prev_block; + var decl = []; + 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; + } + } 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); + default_branch = null; + } + } else if (exp !== branch.expression && !fallthrough) { + extract_declarations_from_unreachable_code(compressor, branch, decl); continue; } } - break; + if (aborts(branch)) { + var block = make_node(AST_BlockStatement, branch, branch).print_to_string(); + if (!fallthrough && prev_block === block) body[body.length - 1].body = []; + body.push(branch); + prev_block = block; + fallthrough = false; + } else { + body.push(branch); + prev_block = null; + fallthrough = true; + } } - var value = self.expression.evaluate(compressor); - out: if (value !== self.expression) try { - // constant expression - var expression = make_node_from_constant(value, self.expression); - self.expression = best_of_expression(expression, self.expression); - if (!compressor.option("dead_code")) break out; - var in_if = false; - var in_block = false; - var started = false; - var stopped = false; - var ruined = false; - var tt = new TreeTransformer(function(node, descend, in_list){ - if (node instanceof AST_Lambda || node instanceof AST_SimpleStatement) { - // no need to descend these node types - return node; - } - else if (node instanceof AST_Switch && node === self) { - node = node.clone(); - descend(node, this); - return ruined ? node : make_node(AST_BlockStatement, node, { - body: node.body.reduce(function(a, branch){ - return a.concat(branch.body); - }, []) - }).transform(compressor); - } - else if (node instanceof AST_If || node instanceof AST_Try) { - var save = in_if; - in_if = !in_block; - descend(node, this); - in_if = save; - return node; - } - else if (node instanceof AST_StatementWithBody || node instanceof AST_Switch) { - var save = in_block; - in_block = true; - descend(node, this); - in_block = save; - return node; - } - else if (node instanceof AST_Break && this.loopcontrol_target(node.label) === self) { - if (in_if) { - ruined = true; - return node; - } - if (in_block) return node; - stopped = true; - return in_list ? MAP.skip : make_node(AST_EmptyStatement, node); - } - else if (node instanceof AST_SwitchBranch && this.parent() === self) { - if (stopped) return MAP.skip; - if (node instanceof AST_Case) { - var exp = node.expression.evaluate(compressor); - if (exp === node.expression) { - // got a case with non-constant expression, baling out - throw self; - } - if (exp === value || started) { - started = true; - if (aborts(node)) stopped = true; - descend(node, this); - return node; - } - return MAP.skip; - } - descend(node, this); - return node; - } + 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); + if (body.length > 0) { + body[0].body = decl.concat(body[0].body); + } + self.body = body; + while (branch = body[body.length - 1]) { + var stat = branch.body[branch.body.length - 1]; + if (stat instanceof AST_Break && compressor.loopcontrol_target(stat.label) === self) + branch.body.pop(); + if (branch.body.length || branch instanceof AST_Case + && (default_branch || branch.expression.has_side_effects(compressor))) break; + if (body.pop() === default_branch) default_branch = null; + } + if (body.length == 0) { + return make_node(AST_BlockStatement, self, { + body: decl.concat(make_node(AST_SimpleStatement, self.expression, { + body: self.expression + })) + }).optimize(compressor); + } + if (body.length == 1 && (body[0] === exact_match || body[0] === default_branch)) { + var has_break = false; + var tw = new TreeWalker(function(node) { + if (has_break + || node instanceof AST_Lambda + || node instanceof AST_SimpleStatement) return true; + if (node instanceof AST_Break && tw.loopcontrol_target(node.label) === self) + has_break = true; }); - tt.stack = compressor.stack.slice(); // so that's able to see parent nodes - self = self.transform(tt); - } catch(ex) { - if (ex !== self) throw ex; + self.walk(tw); + if (!has_break) return make_node(AST_BlockStatement, self, body[0]).optimize(compressor); } return self; }); - OPT(AST_Case, function(self, compressor){ - self.body = tighten_body(self.body, compressor); - return self; - }); - OPT(AST_Try, function(self, compressor){ self.body = tighten_body(self.body, compressor); return self; @@ -3056,7 +3068,9 @@ merge(Compressor.prototype, { && (self.car.operator == "++" || self.car.operator == "--")) { left = self.car.expression; } - if (left) { + if (left + && !(left instanceof AST_SymbolRef + && left.definition().orig[0] instanceof AST_SymbolLambda)) { var parent, field; var cdr = self.cdr; while (true) { @@ -3072,9 +3086,14 @@ merge(Compressor.prototype, { return car; } if (cdr instanceof AST_Binary && !(cdr instanceof AST_Assign)) { - field = cdr.left.is_constant() ? "right" : "left"; + if (cdr.left.is_constant()) { + if (cdr.operator == "||" || cdr.operator == "&&") break; + field = "right"; + } else { + field = "left"; + } } else if (cdr instanceof AST_Call - || cdr instanceof AST_Unary && cdr.operator != "++" && cdr.operator != "--") { + || cdr instanceof AST_Unary && !unary_side_effects(cdr.operator)) { field = "expression"; } else break; parent = cdr; @@ -3139,10 +3158,10 @@ merge(Compressor.prototype, { // typeof always returns a non-empty string, thus it's // always true in booleans compressor.warn("Boolean expression always true [{file}:{line},{col}]", self.start); - return make_node(AST_Seq, self, { + return (e instanceof AST_SymbolRef ? make_node(AST_True, self) : make_node(AST_Seq, self, { car: e, cdr: make_node(AST_True, self) - }).optimize(compressor); + })).optimize(compressor); } } // avoids infinite recursion of numerals @@ -3424,6 +3443,7 @@ merge(Compressor.prototype, { left: self.left, right: self.right.expression }); + break; } // -a + b => b - a if (self.left instanceof AST_UnaryPrefix @@ -3435,6 +3455,7 @@ merge(Compressor.prototype, { left: self.right, right: self.left.expression }); + break; } case "*": associative = compressor.option("unsafe_math"); @@ -3698,21 +3719,30 @@ merge(Compressor.prototype, { alternative: self.consequent }); } + var condition = self.condition; var consequent = self.consequent; var alternative = self.alternative; + // x?x:y --> x||y + if (condition instanceof AST_SymbolRef + && consequent instanceof AST_SymbolRef + && condition.definition() === consequent.definition()) { + return make_node(AST_Binary, self, { + operator: "||", + left: condition, + right: alternative + }); + } + // if (foo) exp = something; else exp = something_else; + // | + // v + // exp = foo ? something : something_else; if (consequent instanceof AST_Assign && alternative instanceof AST_Assign && consequent.operator == alternative.operator && consequent.left.equivalent_to(alternative.left) - && (!consequent.left.has_side_effects(compressor) - || !self.condition.has_side_effects(compressor)) - ) { - /* - * Stuff like this: - * if (foo) exp = something; else exp = something_else; - * ==> - * exp = foo ? something : something_else; - */ + && (!self.condition.has_side_effects(compressor) + || consequent.operator == "=" + && !consequent.left.has_side_effects(compressor))) { return make_node(AST_Assign, self, { operator: consequent.operator, left: consequent.left, diff --git a/lib/output.js b/lib/output.js index 7e846f7d..b174c08b 100644 --- a/lib/output.js +++ b/lib/output.js @@ -904,7 +904,7 @@ function OutputStream(options) { output.print("for"); output.space(); output.with_parens(function(){ - if (self.init && !(self.init instanceof AST_EmptyStatement)) { + if (self.init) { if (self.init instanceof AST_Definitions) { self.init.print(output); } else { diff --git a/lib/parse.js b/lib/parse.js index 4c410040..3b167044 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -852,6 +852,7 @@ function parse($TEXT, options) { html5_comments : true, bare_returns : false, shebang : true, + cli : false, }); var S = { @@ -2418,7 +2419,7 @@ function parse($TEXT, options) { function make_unary(ctor, op, expr) { if ((op == "++" || op == "--") && !is_assignable(expr)) - croak("Invalid use of " + op + " operator"); + croak("Invalid use of " + op + " operator", null, ctor === AST_UnaryPrefix ? expr.start.col - 1 : null); return new ctor({ operator: op, expression: expr }); }; @@ -2467,6 +2468,7 @@ function parse($TEXT, options) { }; function is_assignable(expr) { + if (options.cli) return true; return expr instanceof AST_PropAccess || expr instanceof AST_SymbolRef; }; diff --git a/lib/scope.js b/lib/scope.js index 87a56e0c..185ef755 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -83,9 +83,16 @@ SymbolDef.prototype = { } else if (!this.mangled_name && !this.unmangleable(options)) { var s = this.scope; - if (!options.screw_ie8 && this.orig[0] instanceof AST_SymbolLambda) + var sym = this.orig[0]; + if (!options.screw_ie8 && sym instanceof AST_SymbolLambda) s = s.parent_scope; - this.mangled_name = s.next_mangled(options, this); + var def; + if (options.screw_ie8 + && sym instanceof AST_SymbolCatch + && (def = s.parent_scope.find_variable(sym))) { + this.mangled_name = def.mangled_name || def.name; + } else + this.mangled_name = s.next_mangled(options, this); if (this.global && cache) { cache.set(this.name, this.mangled_name); } @@ -111,8 +118,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ if (node.is_block_scope()) { var save_scope = scope; scope = new AST_Scope(node); - scope.init_scope_vars(); - scope.parent_scope = save_scope; + scope.init_scope_vars(save_scope); if (!(node instanceof AST_Scope)) { scope.uses_with = save_scope.uses_with; scope.uses_eval = save_scope.uses_eval; @@ -129,8 +135,8 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ return true; } if (node instanceof AST_Scope) { - node.init_scope_vars(); - var save_scope = node.parent_scope = scope; + node.init_scope_vars(scope); + var save_scope = scope; var save_defun = defun; var save_labels = labels; defun = scope = node; @@ -213,6 +219,14 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ || node instanceof AST_SymbolConst) { var def = ((node instanceof AST_SymbolBlockDeclaration) ? scope : defun).def_variable(node, in_export, in_block); def.destructuring = in_destructuring; + if (defun !== scope) { + node.mark_enclosed(options); + var def = scope.find_variable(node); + if (node.thedef !== def) { + node.thedef = def; + node.reference(options); + } + } } else if (node instanceof AST_SymbolCatch) { scope.def_variable(node, in_export, in_block); @@ -309,14 +323,14 @@ AST_Toplevel.DEFMETHOD("def_global", function(node){ } }); -AST_Scope.DEFMETHOD("init_scope_vars", function(){ - this.variables = new Dictionary(); // map name to AST_SymbolVar (variables defined in this scope; includes functions) - this.functions = new Dictionary(); // map name to AST_SymbolDefun (functions defined in this scope) - this.uses_with = false; // will be set to true if this or some nested scope uses the `with` statement - this.uses_eval = false; // will be set to true if this or nested scope uses the global `eval` - this.parent_scope = null; // the parent scope - this.enclosed = []; // a list of variables from this or outer scope(s) that are referenced from this or inner scopes - this.cname = -1; // the current index for mangling functions/variables +AST_Scope.DEFMETHOD("init_scope_vars", function(parent_scope){ + this.variables = new Dictionary(); // map name to AST_SymbolVar (variables defined in this scope; includes functions) + this.functions = new Dictionary(); // map name to AST_SymbolDefun (functions defined in this scope) + this.uses_with = false; // will be set to true if this or some nested scope uses the `with` statement + this.uses_eval = false; // will be set to true if this or nested scope uses the global `eval` + this.parent_scope = parent_scope; // the parent scope + this.enclosed = []; // a list of variables from this or outer scope(s) that are referenced from this or inner scopes + this.cname = -1; // the current index for mangling functions/variables }); AST_Node.DEFMETHOD("is_block_scope", function(){ @@ -339,15 +353,15 @@ AST_IterationStatement.DEFMETHOD("is_block_scope", function(){ AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; - - var symbol = new AST_VarDef({ name: "arguments", start: this.start, end: this.end }); - var def = new SymbolDef(this, this.variables.size(), symbol); - this.variables.set(symbol.name, def); + this.def_variable(new AST_SymbolVar({ + name: "arguments", + start: this.start, + end: this.end + })); }); -AST_SymbolRef.DEFMETHOD("reference", function(options) { +AST_Symbol.DEFMETHOD("mark_enclosed", function(options) { var def = this.definition(); - def.references.push(this); var s = this.scope; while (s) { push_uniq(s.enclosed, def); @@ -361,6 +375,11 @@ AST_SymbolRef.DEFMETHOD("reference", function(options) { } }); +AST_Symbol.DEFMETHOD("reference", function(options) { + this.definition().references.push(this); + this.mark_enclosed(options); +}); + AST_Scope.DEFMETHOD("find_variable", function(name){ if (name instanceof AST_Symbol) name = name.name; return this.variables.get(name) diff --git a/package.json b/package.json index f095c793..2ada98ea 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.15", + "version": "2.8.17", "engines": { "node": ">=0.8.0" }, diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index dad2adf2..07dcb44a 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -894,7 +894,8 @@ collapse_vars_unary: { } expect: { function f0(o, p) { - delete o[p]; + var x = o[p]; + delete x; } function f1(n) { return n > +!!n @@ -1621,3 +1622,71 @@ issue_1631_3: { } expect_stdout: "6" } + +var_side_effects_1: { + options = { + collapse_vars: true, + } + input: { + var print = console.log.bind(console); + function foo(x) { + var twice = x * 2; + print('Foo:', twice); + } + foo(10); + } + expect: { + var print = console.log.bind(console); + function foo(x) { + print('Foo:', 2 * x); + } + foo(10); + } + expect_stdout: true +} + +var_side_effects_2: { + options = { + collapse_vars: true, + } + input: { + var print = console.log.bind(console); + function foo(x) { + var twice = x.y * 2; + print('Foo:', twice); + } + foo({ y: 10 }); + } + expect: { + var print = console.log.bind(console); + function foo(x) { + var twice = 2 * x.y; + print('Foo:', twice); + } + foo({ y: 10 }); + } + expect_stdout: true +} + +var_side_effects_3: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + var print = console.log.bind(console); + function foo(x) { + var twice = x.y * 2; + print('Foo:', twice); + } + foo({ y: 10 }); + } + expect: { + var print = console.log.bind(console); + function foo(x) { + print('Foo:', 2 * x.y); + } + foo({ y: 10 }); + } + expect_stdout: true +} diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 7c81cc80..e7ea2bb2 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -893,3 +893,72 @@ equality_conditionals_true: { } expect_stdout: true } + +issue_1645_1: { + options = { + conditionals: true, + } + input: { + var a = 100, b = 10; + (b = a) ? a++ + (b += a) ? b += a : b += a : b ^= a; + console.log(a, b); + } + expect: { + var a = 100, b = 10; + (b = a) ? (a++ + (b += a), b += a) : b ^= a; + console.log(a,b); + } + expect_stdout: true +} + +issue_1645_2: { + options = { + conditionals: true, + } + input: { + var a = 0; + function f() { + return a++; + } + f() ? a += 2 : a += 4; + console.log(a); + } + expect: { + var a = 0; + function f(){ + return a++; + } + f() ? a += 2 : a += 4; + console.log(a); + } + expect_stdout: true +} + +condition_symbol_matches_consequent: { + options = { + conditionals: true, + } + input: { + function foo(x, y) { + return x ? x : y; + } + function bar() { + return g ? g : h; + } + var g = 4; + var h = 5; + console.log(foo(3, null), foo(0, 7), foo(true, false), bar()); + } + expect: { + function foo(x, y) { + return x || y; + } + function bar() { + return g || h; + } + var g = 4; + var h = 5; + console.log(foo(3, null), foo(0, 7), foo(true, false), bar()); + } + expect_stdout: "3 7 true 4" +} diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index d59feb52..796d5598 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -872,3 +872,143 @@ issue_1583: { } } } + +issue_1656: { + options = { + toplevel: true, + unused: true, + } + beautify = { + beautify: true, + } + input: { + for(var a=0;;); + } + expect_exact: "for (;;) ;" +} + +issue_1709: { + options = { + unused: true, + } + input: { + console.log( + function x() { + var x = 1; + return x; + }(), + function y() { + const y = 2; + return y; + }(), + function z() { + function z() {} + return z; + }() + ); + } + expect: { + console.log( + function() { + var x = 1; + return x; + }(), + function() { + const y = 2; + return y; + }(), + function() { + function z() {} + return z; + }() + ); + } + expect_stdout: true +} + +issue_1715_1: { + options = { + unused: true, + } + input: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a; + } + } + f(); + console.log(a); + } + expect: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a; + } + } + f(); + console.log(a); + } + expect_stdout: "1" +} + +issue_1715_2: { + options = { + unused: true, + } + input: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a = 2; + } + } + f(); + console.log(a); + } + expect: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a; + } + } + f(); + console.log(a); + } + expect_stdout: "1" +} + +issue_1715_3: { + options = { + unused: true, + } + input: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a = 2 + x(); + } + } + f(); + console.log(a); + } + expect: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a = x(); + } + } + f(); + console.log(a); + } + expect_stdout: "1" +} diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 6b8f0826..55735ee4 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -883,3 +883,16 @@ unsafe_charAt_noop: { ); } } + +issue_1649: { + options = { + evaluate: true, + } + input: { + console.log(-1 + -1); + } + expect: { + console.log(-2); + } + expect_stdout: "-2"; +} diff --git a/test/compress/issue-1639.js b/test/compress/issue-1639.js new file mode 100644 index 00000000..b6a9647f --- /dev/null +++ b/test/compress/issue-1639.js @@ -0,0 +1,88 @@ + +issue_1639_1: { + options = { + booleans: true, + cascade: true, + conditionals: true, + evaluate: true, + join_vars: true, + loops: true, + sequences: true, + side_effects: true, + } + input: { + var a = 100, b = 10; + + var L1 = 5; + while (--L1 > 0) { + if ((--b), false) { + if (b) { + var ignore = 0; + } + } + } + + console.log(a, b); + } + expect: { + for (var a = 100, b = 10, L1 = 5; --L1 > 0;) + if (--b, !1) var ignore = 0; + console.log(a, b); + } + expect_stdout: true +} + +issue_1639_2: { + options = { + booleans: true, + cascade: true, + conditionals: true, + evaluate: true, + join_vars: true, + sequences: true, + side_effects: true, + } + input: { + var a = 100, b = 10; + + function f19() { + if (++a, false) + if (a) + if (++a); + } + f19(); + + console.log(a, b); + } + expect: { + var a = 100, b = 10; + function f19() { + ++a, 1; + } + f19(), + console.log(a, b); + } + expect_stdout: true +} + +issue_1639_3: { + options = { + booleans: true, + cascade: true, + conditionals: true, + evaluate: true, + sequences: true, + side_effects: true, + } + input: { + var a = 100, b = 10; + a++ && false && a ? 0 : 0; + console.log(a, b); + } + expect: { + var a = 100, b = 10; + a++, + console.log(a, b); + } + expect_stdout: true +} diff --git a/test/compress/issue-1656.js b/test/compress/issue-1656.js new file mode 100644 index 00000000..8b683a28 --- /dev/null +++ b/test/compress/issue-1656.js @@ -0,0 +1,45 @@ +f7: { + options = { + booleans: true, + cascade: true, + collapse_vars: true, + comparisons: true, + conditionals: true, + dead_code: true, + drop_debugger: true, + evaluate: true, + hoist_funs: true, + if_return: true, + join_vars: true, + loops: true, + negate_iife: true, + passes: 3, + properties: true, + reduce_vars: true, + sequences: true, + side_effects: true, + toplevel: true, + unused: true, + } + beautify = { + beautify: true, + } + input: { + var a = 100, b = 10; + function f22464() { + var brake146670 = 5; + while (((b = a) ? !a : ~a ? null : b += a) && --brake146670 > 0) { + } + } + f22464(); + console.log(a, b); + } + expect_exact: [ + "var a = 100, b = 10;", + "", + "!function() {", + " for (;b = a, !1; ) ;", + "}(), console.log(a, b);", + ] + expect_stdout: true +} diff --git a/test/compress/issue-1673.js b/test/compress/issue-1673.js new file mode 100644 index 00000000..4628e37c --- /dev/null +++ b/test/compress/issue-1673.js @@ -0,0 +1,159 @@ +side_effects_catch: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f() { + function g() { + try { + throw 0; + } catch (e) { + console.log("PASS"); + } + } + g(); + } + f(); + } + expect: { + function f() { + (function() { + try { + throw 0; + } catch (e) { + console.log("PASS"); + } + })(); + } + f(); + } + expect_stdout: "PASS" +} + +side_effects_else: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f(x) { + function g() { + if (x); + else console.log("PASS"); + } + g(); + } + f(0); + } + expect: { + function f(x) { + (function() { + if (x); + else console.log("PASS"); + })(); + } + f(0); + } + expect_stdout: "PASS" +} + +side_effects_finally: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f() { + function g() { + try { + } catch (e) { + } finally { + console.log("PASS"); + } + } + g(); + } + f(); + } + expect: { + function f() { + (function() { + try { + } catch (e) { + } finally { + console.log("PASS"); + } + })(); + } + f(); + } + expect_stdout: "PASS" +} + +side_effects_label: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f(x) { + function g() { + L: { + console.log("PASS"); + break L; + } + } + g(); + } + f(0); + } + expect: { + function f(x) { + (function() { + L: { + console.log("PASS"); + break L; + } + })(); + } + f(0); + } + expect_stdout: "PASS" +} + +side_effects_switch: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f() { + function g() { + switch (0) { + default: + case console.log("PASS"): + } + } + g(); + } + f(); + } + expect: { + function f() { + (function() { + switch (0) { + default: + case console.log("PASS"): + } + })(); + } + f(); + } + expect_stdout: "PASS" +} diff --git a/test/compress/issue-1704.js b/test/compress/issue-1704.js new file mode 100644 index 00000000..a73f7f99 --- /dev/null +++ b/test/compress/issue-1704.js @@ -0,0 +1,347 @@ +mangle_catch: { + options = { + screw_ie8: true, + toplevel: false, + } + mangle = { + screw_ie8: true, + toplevel: false, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + a = "PASS"; + } + console.log(a); + } + expect_exact: 'var a="FAIL";try{throw 1}catch(o){a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_ie8: { + options = { + screw_ie8: false, + toplevel: false, + } + mangle = { + screw_ie8: false, + toplevel: false, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + a = "PASS"; + } + console.log(a); + } + expect_exact: 'var a="FAIL";try{throw 1}catch(args){a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_var: { + options = { + screw_ie8: true, + toplevel: false, + } + mangle = { + screw_ie8: true, + toplevel: false, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + var a = "PASS"; + } + console.log(a); + } + expect_exact: 'var a="FAIL";try{throw 1}catch(o){var a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_var_ie8: { + options = { + screw_ie8: false, + toplevel: false, + } + mangle = { + screw_ie8: false, + toplevel: false, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + var a = "PASS"; + } + console.log(a); + } + expect_exact: 'var a="FAIL";try{throw 1}catch(args){var a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_toplevel: { + options = { + screw_ie8: true, + toplevel: true, + } + mangle = { + screw_ie8: true, + toplevel: true, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + a = "PASS"; + } + console.log(a); + } + expect_exact: 'var o="FAIL";try{throw 1}catch(c){o="PASS"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_ie8_toplevel: { + options = { + screw_ie8: false, + toplevel: true, + } + mangle = { + screw_ie8: false, + toplevel: true, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + a = "PASS"; + } + console.log(a); + } + expect_exact: 'var o="FAIL";try{throw 1}catch(c){o="PASS"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_var_toplevel: { + options = { + screw_ie8: true, + toplevel: true, + } + mangle = { + screw_ie8: true, + toplevel: true, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + var a = "PASS"; + } + console.log(a); + } + expect_exact: 'var o="FAIL";try{throw 1}catch(r){var o="PASS"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_var_ie8_toplevel: { + options = { + screw_ie8: false, + toplevel: true, + } + mangle = { + screw_ie8: false, + toplevel: true, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + var a = "PASS"; + } + console.log(a); + } + expect_exact: 'var o="FAIL";try{throw 1}catch(r){var o="PASS"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_redef_1: { + options = { + screw_ie8: true, + toplevel: false, + } + mangle = { + screw_ie8: true, + toplevel: false, + } + input: { + var a = "PASS"; + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'var a="PASS";try{throw"FAIL1"}catch(a){var a="FAIL2"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_redef_1_ie8: { + options = { + screw_ie8: false, + toplevel: false, + } + mangle = { + screw_ie8: false, + toplevel: false, + } + input: { + var a = "PASS"; + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'var a="PASS";try{throw"FAIL1"}catch(a){var a="FAIL2"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_redef_1_toplevel: { + options = { + screw_ie8: true, + toplevel: true, + } + mangle = { + screw_ie8: true, + toplevel: true, + } + input: { + var a = "PASS"; + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'var o="PASS";try{throw"FAIL1"}catch(o){var o="FAIL2"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_redef_1_ie8_toplevel: { + options = { + screw_ie8: false, + toplevel: true, + } + mangle = { + screw_ie8: false, + toplevel: true, + } + input: { + var a = "PASS"; + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'var o="PASS";try{throw"FAIL1"}catch(o){var o="FAIL2"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_redef_2: { + options = { + screw_ie8: true, + toplevel: false, + } + mangle = { + screw_ie8: true, + toplevel: false, + } + input: { + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'try{throw"FAIL1"}catch(a){var a="FAIL2"}console.log(a);' + expect_stdout: "undefined" +} + +mangle_catch_redef_2_ie8: { + options = { + screw_ie8: false, + toplevel: false, + } + mangle = { + screw_ie8: false, + toplevel: false, + } + input: { + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'try{throw"FAIL1"}catch(a){var a="FAIL2"}console.log(a);' + expect_stdout: "undefined" +} + +mangle_catch_redef_2_toplevel: { + options = { + screw_ie8: true, + toplevel: true, + } + mangle = { + screw_ie8: true, + toplevel: true, + } + input: { + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'try{throw"FAIL1"}catch(o){var o="FAIL2"}console.log(o);' + expect_stdout: "undefined" +} + +mangle_catch_redef_2_ie8_toplevel: { + options = { + screw_ie8: false, + toplevel: true, + } + mangle = { + screw_ie8: false, + toplevel: true, + } + input: { + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'try{throw"FAIL1"}catch(o){var o="FAIL2"}console.log(o);' + expect_stdout: "undefined" +} diff --git a/test/compress/loops.js b/test/compress/loops.js index b55c6162..c8d77840 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -440,3 +440,21 @@ issue_186_beautify_bracketize_ie8: { '}', ] } + +issue_1648: { + options = { + join_vars: true, + loops: true, + passes: 2, + sequences: true, + unused: true, + } + input: { + function f() { + x(); + var b = 1; + while (1); + } + } + expect_exact: "function f(){for(x();1;);}" +} diff --git a/test/compress/negate-iife.js b/test/compress/negate-iife.js index e9ad37db..343e8e16 100644 --- a/test/compress/negate-iife.js +++ b/test/compress/negate-iife.js @@ -353,8 +353,9 @@ issue_1254_negate_iife_nested: { issue_1288: { options = { - negate_iife: true, conditionals: true, + negate_iife: true, + side_effects: false, }; input: { if (w) ; @@ -374,11 +375,11 @@ issue_1288: { })(0); } expect: { - w || function f() {}(); - x || function() { + w || !function f() {}(); + x || !function() { x = {}; }(); - y ? function() {}() : function(z) { + y ? !function() {}() : !function(z) { return z; }(0); } diff --git a/test/compress/numbers.js b/test/compress/numbers.js index 0b40bb9c..ea439ecc 100644 --- a/test/compress/numbers.js +++ b/test/compress/numbers.js @@ -153,3 +153,18 @@ evaluate_4: { ); } } + +issue_1710: { + options = { + evaluate: true, + } + input: { + var x = {}; + console.log((x += 1) + -x); + } + expect: { + var x = {}; + console.log((x += 1) + -x); + } + expect_stdout: true +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 3d5612cf..87942ab9 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -425,7 +425,7 @@ iife_new: { expect_stdout: true } -multi_def: { +multi_def_1: { options = { evaluate: true, reduce_vars: true, @@ -435,7 +435,7 @@ multi_def: { if (a) var b = 1; else - var b = 2 + var b = 2; console.log(b + 1); } } @@ -444,7 +444,7 @@ multi_def: { if (a) var b = 1; else - var b = 2 + var b = 2; console.log(b + 1); } } @@ -479,6 +479,33 @@ multi_def_2: { } } +multi_def_3: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + function f(a) { + var b = 2; + if (a) + var b; + else + var b; + console.log(b + 1); + } + } + expect: { + function f(a) { + var b = 2; + if (a) + var b; + else + var b; + console.log(3); + } + } +} + use_before_var: { options = { evaluate: true, @@ -1364,3 +1391,466 @@ issue_1606: { } } } + +issue_1670_1: { + options = { + comparisons: true, + conditionals: true, + evaluate: true, + dead_code: true, + reduce_vars: true, + unused: true, + } + input: { + (function f() { + switch (1) { + case 0: + var a = true; + break; + default: + if (typeof a === "undefined") console.log("PASS"); + else console.log("FAIL"); + } + })(); + } + expect: { + (function() { + var a; + void 0 === a ? console.log("PASS") : console.log("FAIL"); + })(); + } + expect_stdout: "PASS" +} + +issue_1670_2: { + options = { + conditionals: true, + evaluate: true, + dead_code: true, + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + (function f() { + switch (1) { + case 0: + var a = true; + break; + default: + if (typeof a === "undefined") console.log("PASS"); + else console.log("FAIL"); + } + })(); + } + expect: { + (function() { + console.log("PASS"); + })(); + } + expect_stdout: "PASS" +} + +issue_1670_3: { + options = { + comparisons: true, + conditionals: true, + evaluate: true, + dead_code: true, + reduce_vars: true, + unused: true, + } + input: { + (function f() { + switch (1) { + case 0: + var a = true; + break; + case 1: + if (typeof a === "undefined") console.log("PASS"); + else console.log("FAIL"); + } + })(); + } + expect: { + (function() { + var a; + void 0 === a ? console.log("PASS") : console.log("FAIL"); + })(); + } + expect_stdout: "PASS" +} + +issue_1670_4: { + options = { + conditionals: true, + evaluate: true, + dead_code: true, + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + (function f() { + switch (1) { + case 0: + var a = true; + break; + case 1: + if (typeof a === "undefined") console.log("PASS"); + else console.log("FAIL"); + } + })(); + } + expect: { + (function() { + console.log("PASS"); + })(); + } + expect_stdout: "PASS" +} + +issue_1670_5: { + options = { + dead_code: true, + evaluate: true, + keep_fargs: false, + reduce_vars: true, + unused: true, + } + input: { + (function(a) { + switch (1) { + case a: + console.log(a); + break; + default: + console.log(2); + break; + } + })(1); + } + expect: { + (function() { + console.log(1); + })(); + } + expect_stdout: "1" +} + +issue_1670_6: { + options = { + dead_code: true, + evaluate: true, + keep_fargs: false, + reduce_vars: true, + unused: true, + } + input: { + (function(a) { + switch (1) { + case a = 1: + console.log(a); + break; + default: + console.log(2); + break; + } + })(1); + } + expect: { + (function(a) { + switch (1) { + case a = 1: + console.log(a); + break; + default: + console.log(2); + } + })(1); + } + expect_stdout: "1" +} + +unary_delete: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + var b = 10; + function f() { + var a; + if (delete a) b--; + } + f(); + console.log(b); + } + expect: { + var b = 10; + function f() { + var a; + if (delete a) b--; + } + f(); + console.log(b); + } + expect_stdout: true +} + +redefine_arguments_1: { + options = { + evaluate: true, + keep_fargs: false, + reduce_vars: true, + unused: true, + } + input: { + function f() { + var arguments; + return typeof arguments; + } + function g() { + var arguments = 42; + return typeof arguments; + } + function h(x) { + var arguments = x; + return typeof arguments; + } + console.log(f(), g(), h()); + } + expect: { + function f() { + var arguments; + return typeof arguments; + } + function g() { + return"number"; + } + function h(x) { + var arguments = x; + return typeof arguments; + } + console.log(f(), g(), h()); + } + expect_stdout: "object number undefined" +} + +redefine_arguments_2: { + options = { + evaluate: true, + keep_fargs: false, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + function f() { + var arguments; + return typeof arguments; + } + function g() { + var arguments = 42; + return typeof arguments; + } + function h(x) { + var arguments = x; + return typeof arguments; + } + console.log(f(), g(), h()); + } + expect: { + console.log(function() { + var arguments; + return typeof arguments; + }(), function() { + return"number"; + }(), function(x) { + var arguments = x; + return typeof arguments; + }()); + } + expect_stdout: "object number undefined" +} + +redefine_arguments_3: { + options = { + evaluate: true, + keep_fargs: false, + passes: 3, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + function f() { + var arguments; + return typeof arguments; + } + function g() { + var arguments = 42; + return typeof arguments; + } + function h(x) { + var arguments = x; + return typeof arguments; + } + console.log(f(), g(), h()); + } + expect: { + console.log(function() { + var arguments; + return typeof arguments; + }(), "number", "undefined"); + } + expect_stdout: "object number undefined" +} + +redefine_farg_1: { + options = { + evaluate: true, + keep_fargs: false, + reduce_vars: true, + unused: true, + } + input: { + function f(a) { + var a; + return typeof a; + } + function g(a) { + var a = 42; + return typeof a; + } + function h(a, b) { + var a = b; + return typeof a; + } + console.log(f([]), g([]), h([])); + } + expect: { + function f(a) { + var a; + return typeof a; + } + function g() { + return"number"; + } + function h(a, b) { + var a = b; + return typeof a; + } + console.log(f([]), g([]), h([])); + } + expect_stdout: "object number undefined" +} + +redefine_farg_2: { + options = { + evaluate: true, + keep_fargs: false, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + function f(a) { + var a; + return typeof a; + } + function g(a) { + var a = 42; + return typeof a; + } + function h(a, b) { + var a = b; + return typeof a; + } + console.log(f([]), g([]), h([])); + } + expect: { + console.log(function(a) { + var a; + return typeof a; + }([]), function() { + return "number"; + }(),function(a, b) { + var a = b; + return typeof a; + }([])); + } + expect_stdout: "object number undefined" +} + +redefine_farg_3: { + options = { + evaluate: true, + keep_fargs: false, + passes: 3, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + function f(a) { + var a; + return typeof a; + } + function g(a) { + var a = 42; + return typeof a; + } + function h(a, b) { + var a = b; + return typeof a; + } + console.log(f([]), g([]), h([])); + } + expect: { + console.log(function(a) { + var a; + return typeof a; + }([]), "number", function(a) { + var a = void 0; + return typeof a; + }([])); + } + expect_stdout: "object number undefined" +} + +delay_def: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + function f() { + return a; + var a; + } + function g() { + return a; + var a = 1; + } + console.log(f(), g()); + } + expect: { + function f() { + return a; + var a; + } + function g() { + return a; + var a = 1; + } + console.log(f(), g()); + } + expect_stdout: true +} diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 49b61ae0..f1fa0e87 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -306,3 +306,137 @@ unsafe_undefined: { } } } + +issue_1685: { + options = { + cascade: true, + side_effects: true, + } + input: { + var a = 100, b = 10; + function f() { + var a = (a--, delete a && --b); + } + f(); + console.log(a, b); + } + expect: { + var a = 100, b = 10; + function f() { + var a = (a--, delete a && --b); + } + f(); + console.log(a, b); + } + expect_stdout: true +} + +func_def_1: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f() { + return f = 0, !!f; + } + console.log(f()); + } + expect: { + function f() { + return !!(f = 0); + } + console.log(f()); + } + expect_stdout: "false" +} + +func_def_2: { + options = { + cascade: true, + side_effects: true, + } + input: { + console.log(function f() { + return f = 0, !!f; + }()); + } + expect: { + console.log(function f() { + return f = 0, !!f; + }()); + } + expect_stdout: "true" +} + +func_def_3: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f() { + function g() {} + return g = 0, !!g; + } + console.log(f()); + } + expect: { + function f() { + function g() {} + return !!(g = 0); + } + console.log(f()); + } + expect_stdout: "false" +} + +func_def_4: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f() { + function g() { + return g = 0, !!g; + } + return g(); + } + console.log(f()); + } + expect: { + function f() { + function g() { + return !!(g = 0); + } + return g(); + } + console.log(f()); + } + expect_stdout: "false" +} + +func_def_5: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f() { + return function g(){ + return g = 0, !!g; + }(); + } + console.log(f()); + } + expect: { + function f() { + return function g(){ + return g = 0, !!g; + }(); + } + console.log(f()); + } + expect_stdout: "true" +} diff --git a/test/compress/switch.js b/test/compress/switch.js index 62e39cf7..5c12449c 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -258,3 +258,425 @@ keep_default: { } } } + +issue_1663: { + options = { + dead_code: true, + evaluate: true, + } + input: { + var a = 100, b = 10; + function f() { + switch (1) { + case 1: + b = a++; + return ++b; + default: + var b; + } + } + f(); + console.log(a, b); + } + expect: { + var a = 100, b = 10; + function f() { + var b; + b = a++; + return ++b; + } + f(); + console.log(a, b); + } + expect_stdout: true +} + +drop_case: { + options = { + dead_code: true, + } + input: { + switch (foo) { + case 'bar': baz(); break; + case 'moo': + break; + } + } + expect: { + switch (foo) { + case 'bar': baz(); + } + } +} + +keep_case: { + options = { + dead_code: true, + } + input: { + switch (foo) { + case 'bar': baz(); break; + case moo: + break; + } + } + expect: { + switch (foo) { + case 'bar': baz(); break; + case moo: + } + } +} + +issue_376: { + options = { + dead_code: true, + evaluate: true, + } + input: { + switch (true) { + case boolCondition: + console.log(1); + break; + case false: + console.log(2); + break; + } + } + expect: { + switch (true) { + case boolCondition: + console.log(1); + } + } +} + +issue_441_1: { + options = { + dead_code: true, + } + input: { + switch (foo) { + case bar: + qux(); + break; + case baz: + qux(); + break; + default: + qux(); + break; + } + } + expect: { + switch (foo) { + case bar: + case baz: + default: + qux(); + } + } +} + +issue_441_2: { + options = { + dead_code: true, + } + input: { + switch (foo) { + case bar: + // TODO: Fold into the case below + qux(); + break; + case fall: + case baz: + qux(); + break; + default: + qux(); + break; + } + } + expect: { + switch (foo) { + case bar: + qux(); + break; + case fall: + case baz: + default: + qux(); + } + } +} + +issue_1674: { + options = { + dead_code: true, + evaluate: true, + } + input: { + switch (0) { + default: + console.log("FAIL"); + break; + case 0: + console.log("PASS"); + break; + } + } + expect: { + console.log("PASS"); + } + expect_stdout: "PASS" +} + +issue_1679: { + options = { + dead_code: true, + evaluate: true, + } + input: { + var a = 100, b = 10; + function f() { + switch (--b) { + default: + case !function x() {}: + break; + case b--: + switch (0) { + default: + case a--: + } + break; + case (a++): + break; + } + } + f(); + console.log(a, b); + } + expect: { + var a = 100, b = 10; + function f() { + switch (--b) { + default: + case !function x() {}: + break; + case b--: + switch (0) { + default: + case a--: + } + break; + case (a++): + } + } + f(); + console.log(a, b); + } + expect_stdout: true +} + +issue_1680_1: { + options = { + dead_code: true, + evaluate: true, + } + input: { + function f(x) { + console.log(x); + return x + 1; + } + switch (2) { + case f(0): + case f(1): + f(2); + case 2: + case f(3): + case f(4): + f(5); + } + } + expect: { + function f(x) { + console.log(x); + return x + 1; + } + switch (2) { + case f(0): + case f(1): + f(2); + case 2: + f(5); + } + } + expect_stdout: [ + "0", + "1", + "2", + "5", + ] +} + +issue_1680_2: { + options = { + dead_code: true, + } + input: { + var a = 100, b = 10; + switch (b) { + case a--: + break; + case b: + var c; + break; + case a: + break; + case a--: + break; + } + console.log(a, b); + } + expect: { + var a = 100, b = 10; + switch (b) { + case a--: + break; + case b: + var c; + break; + case a: + case a--: + } + console.log(a, b); + } + expect_stdout: true +} + +issue_1690_1: { + options = { + dead_code: true, + } + input: { + switch (console.log("PASS")) {} + } + expect: { + console.log("PASS"); + } + expect_stdout: "PASS" +} + +issue_1690_2: { + options = { + dead_code: false, + } + input: { + switch (console.log("PASS")) {} + } + expect: { + switch (console.log("PASS")) {} + } + expect_stdout: "PASS" +} + +if_switch_typeof: { + options = { + conditionals: true, + dead_code: true, + side_effects: true, + } + input: { + if (a) switch(typeof b) {} + } + expect: { + a; + } +} + +issue_1698: { + options = { + side_effects: true, + } + input: { + var a = 1; + !function() { + switch (a++) {} + }(); + console.log(a); + } + expect: { + var a = 1; + !function() { + switch (a++) {} + }(); + console.log(a); + } + expect_stdout: "2" +} + +issue_1705_1: { + options = { + dead_code: true, + } + input: { + var a = 0; + switch (a) { + default: + console.log("FAIL"); + case 0: + break; + } + } + expect: { + var a = 0; + switch (a) { + default: + console.log("FAIL"); + case 0: + } + } + expect_stdout: true +} + +issue_1705_2: { + options = { + dead_code: true, + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a = 0; + switch (a) { + default: + console.log("FAIL"); + case 0: + break; + } + } + expect: { + } + expect_stdout: true +} + +issue_1705_3: { + options = { + dead_code: true, + } + input: { + switch (a) { + case 0: + break; + default: + break; + } + } + expect: { + a; + } + expect_stdout: true +} diff --git a/test/compress/transform.js b/test/compress/transform.js index 1cc72c07..48aa605e 100644 --- a/test/compress/transform.js +++ b/test/compress/transform.js @@ -30,7 +30,6 @@ booleans_global_defs: { expect: { console.log(!0); } - expect_stdout: true } condition_evaluate: { diff --git a/test/compress/typeof.js b/test/compress/typeof.js index 7bf8e5e3..60f3d1d0 100644 --- a/test/compress/typeof.js +++ b/test/compress/typeof.js @@ -48,3 +48,15 @@ typeof_in_boolean_context: { foo(); } } + +issue_1668: { + options = { + booleans: true, + } + input: { + if (typeof bar); + } + expect: { + if (!0); + } +} diff --git a/test/input/invalid/assign_1.js b/test/input/invalid/assign_1.js new file mode 100644 index 00000000..6d09d132 --- /dev/null +++ b/test/input/invalid/assign_1.js @@ -0,0 +1 @@ +console.log(1 || 5--); diff --git a/test/input/invalid/assign_2.js b/test/input/invalid/assign_2.js new file mode 100644 index 00000000..197bdc90 --- /dev/null +++ b/test/input/invalid/assign_2.js @@ -0,0 +1 @@ +console.log(2 || (Math.random() /= 2)); diff --git a/test/input/invalid/assign_3.js b/test/input/invalid/assign_3.js new file mode 100644 index 00000000..7c560e4b --- /dev/null +++ b/test/input/invalid/assign_3.js @@ -0,0 +1 @@ +console.log(3 || ++this); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 2b44c901..b956309a 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -251,4 +251,59 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should support hyphen as shorthand", function(done) { + var command = uglifyjscmd + ' test/input/issue-1431/sample.js -m keep-fnames=true'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "function f(r){return function(){function n(n){return n*n}return r(n)}}function g(n){return n(1)+n(2)}console.log(f(g)()==5);\n"); + done(); + }); + }); + it("Should throw syntax error (5--)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/assign_1.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/assign_1.js:1,18", + "console.log(1 || 5--);", + " ^", + "SyntaxError: Invalid use of -- operator" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (Math.random() /= 2)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/assign_2.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/assign_2.js:1,32", + "console.log(2 || (Math.random() /= 2));", + " ^", + "SyntaxError: Invalid assignment" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (++this)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/assign_3.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/assign_3.js:1,18", + "console.log(3 || ++this);", + " ^", + "SyntaxError: Invalid use of ++ operator" + ].join("\n")); + done(); + }); + }); }); diff --git a/test/mocha/glob.js b/test/mocha/glob.js index e291efc8..e9555a52 100644 --- a/test/mocha/glob.js +++ b/test/mocha/glob.js @@ -5,7 +5,7 @@ var path = require("path"); describe("minify() with input file globs", function() { it("minify() with one input file glob string.", function() { var result = Uglify.minify("test/input/issue-1242/foo.*"); - assert.strictEqual(result.code, 'function foo(o){var n=2*o;print("Foo:",n)}var print=console.log.bind(console);'); + assert.strictEqual(result.code, 'function foo(o){print("Foo:",2*o)}var print=console.log.bind(console);'); }); it("minify() with an array of one input file glob.", function() { var result = Uglify.minify([ @@ -20,7 +20,7 @@ describe("minify() with input file globs", function() { ], { compress: { toplevel: true } }); - assert.strictEqual(result.code, 'var print=console.log.bind(console),a=function(n){return 3*n}(3),b=function(n){return n/2}(12);print("qux",a,b),function(n){var o=2*n;print("Foo:",o)}(11);'); + assert.strictEqual(result.code, 'var print=console.log.bind(console),a=function(n){return 3*n}(3),b=function(n){return n/2}(12);print("qux",a,b),function(n){print("Foo:",2*n)}(11);'); }); it("should throw with non-matching glob string", function() { var glob = "test/input/issue-1242/blah.*"; diff --git a/test/run-tests.js b/test/run-tests.js index 09e70021..3d291416 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -105,6 +105,23 @@ function run_compress_tests() { function test_case(test) { log_test(test.name); U.base54.reset(); + var output_options = test.beautify || {}; + var expect; + if (test.expect) { + expect = make_code(as_toplevel(test.expect, test.mangle), output_options); + } else { + expect = test.expect_exact; + } + var input = as_toplevel(test.input, test.mangle); + var input_code = make_code(input, output_options); + var input_formatted = make_code(test.input, { + beautify: true, + quote_style: 3, + keep_quoted_props: true + }); + if (test.mangle_props) { + input = U.mangle_properties(input, test.mangle_props); + } var options = U.defaults(test.options, { warnings: false }); @@ -117,22 +134,6 @@ function run_compress_tests() { if (!options.warnings) options.warnings = true; } var cmp = new U.Compressor(options, true); - var output_options = test.beautify || {}; - var expect; - if (test.expect) { - expect = make_code(as_toplevel(test.expect, test.mangle), output_options); - } else { - expect = test.expect_exact; - } - var input = as_toplevel(test.input, test.mangle); - var input_code = make_code(test.input, { - beautify: true, - quote_style: 3, - keep_quoted_props: true - }); - if (test.mangle_props) { - input = U.mangle_properties(input, test.mangle_props); - } var output = cmp.compress(input); output.figure_out_scope(test.mangle); if (test.mangle) { @@ -142,7 +143,7 @@ function run_compress_tests() { output = make_code(output, output_options); if (expect != output) { log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED---\n{expected}\n\n", { - input: input_code, + input: input_formatted, output: output, expected: expect }); @@ -155,7 +156,7 @@ function run_compress_tests() { var reparsed_ast = U.parse(output); } catch (ex) { log("!!! Test matched expected result but cannot parse output\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--REPARSE ERROR--\n{error}\n\n", { - input: input_code, + input: input_formatted, output: output, error: ex.toString(), }); @@ -174,7 +175,7 @@ function run_compress_tests() { var actual_warnings = JSON.stringify(warnings_emitted); if (expected_warnings != actual_warnings) { log("!!! failed\n---INPUT---\n{input}\n---EXPECTED WARNINGS---\n{expected_warnings}\n---ACTUAL WARNINGS---\n{actual_warnings}\n\n", { - input: input_code, + input: input_formatted, expected_warnings: expected_warnings, actual_warnings: actual_warnings, }); @@ -183,13 +184,13 @@ function run_compress_tests() { } } if (test.expect_stdout) { - var stdout = run_code(make_code(input, output_options)); + var stdout = run_code(input_code); if (test.expect_stdout === true) { test.expect_stdout = stdout; } if (!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_code, + input: input_formatted, expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", expected: test.expect_stdout, actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", @@ -201,7 +202,7 @@ function run_compress_tests() { stdout = run_code(output); if (!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_code, + input: input_formatted, expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", expected: test.expect_stdout, actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", diff --git a/test/ufuzz.js b/test/ufuzz.js new file mode 100644 index 00000000..c1ac8f4c --- /dev/null +++ b/test/ufuzz.js @@ -0,0 +1,477 @@ +// ufuzz.js +// derived from https://github.com/qfox/uglyfuzzer by Peter van der Zee +"use strict"; + +// workaround for tty output truncation upon process.exit() +[process.stdout, process.stderr].forEach(function(stream){ + if (stream._handle && stream._handle.setBlocking) + stream._handle.setBlocking(true); +}); + +var vm = require("vm"); +var minify = require("..").minify; + +var MAX_GENERATED_FUNCTIONS_PER_RUN = 1; +var MAX_GENERATION_RECURSION_DEPTH = 15; +var INTERVAL_COUNT = 100; + +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"' ]; + +var BINARY_OPS_NO_COMMA = [ + ' + ', // 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 + ' - ', + ' + ' ]; + +var NO_COMMA = true; +var MAYBE = true; +var NESTED = true; +var CAN_THROW = true; +var CANNOT_THROW = false; +var CAN_BREAK = true; +var CAN_CONTINUE = 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 + +var TYPEOF_OUTCOMES = [ + 'undefined', + 'string', + 'number', + 'object', + 'boolean', + 'special', + 'unknown', + 'symbol', + 'crap' ]; + +var FUNC_TOSTRING = [ + "Function.prototype.toString = function() {", + " var ids = [];", + " return function() {", + " var i = ids.indexOf(this);", + " if (i < 0) {", + " i = ids.length;", + " ids.push(this);", + " }", + ' return "[Function: __func_" + i + "__]";', + " }", + "}();", + "" +].join("\n"); + +function run_code(code) { + var stdout = ""; + var original_write = process.stdout.write; + process.stdout.write = function(chunk) { + stdout += chunk; + }; + try { + new vm.Script(FUNC_TOSTRING + code).runInNewContext({ + console: { + log: function() { + return console.log.apply(console, [].map.call(arguments, function(arg) { + return typeof arg == "function" ? "[Function]" : arg; + })); + } + } + }, { timeout: 5000 }); + return stdout; + } catch (ex) { + return ex; + } finally { + process.stdout.write = original_write; + } +} + +function rng(max) { + return Math.floor(max * Math.random()); +} + +function createFunctionDecls(n, recurmax, nested) { + if (--recurmax < 0) { return ';'; } + var s = ''; + while (n-- > 0) { + s += createFunctionDecl(recurmax, nested) + '\n'; + } + return s; +} + +var funcs = 0; +function createFunctionDecl(recurmax, nested) { + if (--recurmax < 0) { return ';'; } + var func = funcs++; + var name = rng(5) > 0 ? 'f' + func : createVarName(); + if (name === 'a' || name === 'b') name = 'f' + func; // quick hack to prevent assignment to func names of being called + if (!nested && name === 'undefined' || name === 'NaN' || name === 'Infinity') name = 'f' + func; // cant redefine these in global space + var s = ''; + if (rng(5) === 1) { + // functions with functions. lower the recursion to prevent a mess. + s = 'function ' + name + '(){' + createFunctionDecls(rng(5) + 1, Math.ceil(recurmax / 2), NESTED) + '}\n'; + } else { + // functions with statements + s = 'function ' + name + '(){' + createStatements(3, recurmax) + '}\n'; + } + + if (nested) s = '!' + nested; // avoid "function statements" (decl inside statements) + else s += name + '();' + + return s; +} + +function createStatements(n, recurmax, canThrow, canBreak, canContinue) { + if (--recurmax < 0) { return ';'; } + var s = ''; + while (--n > 0) { + s += createStatement(recurmax, canThrow, canBreak, canContinue); + } + return s; +} + +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'; + } + } + return s; +} + +function createExpression(recurmax, noComma) { + if (--recurmax < 0) { + return createValue(); // note: should return a simple non-recursing expression value! + } + switch (rng(12)) { + case 0: + return '(' + createUnaryOp() + (rng(2) === 1 ? 'a' : 'b') + ')'; + case 1: + return '(a' + (rng(2) == 1 ? '++' : '--') + ')'; + case 2: + return '(b ' + createAssignment() + ' a)'; + case 3: + return '(' + rng(2) + ' === 1 ? a : b)'; + case 4: + return createExpression(recurmax, noComma) + createBinaryOp(noComma) + createExpression(recurmax, noComma); + case 5: + return createValue(); + case 6: + return '(' + createExpression(recurmax) + ')'; + case 7: + return createExpression(recurmax, noComma) + '?(' + createExpression(recurmax) + '):(' + createExpression(recurmax) + ')'; + case 8: + switch(rng(4)) { + case 0: + return '(function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '})()'; + case 1: + return '+function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; + case 2: + return '!function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; + case 3: + return 'void function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; + default: + return 'void function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; + } + case 9: + return createTypeofExpr(recurmax); + case 10: + // you could statically infer that this is just `Math`, regardless of the other expression + // I don't think Uglify does this at this time... + return ''+ + 'new function(){ \n' + + (rng(2) === 1 ? createExpression(recurmax) + '\n' : '') + + 'return Math;\n' + + '}'; + case 11: + // more like a parser test but perhaps comment nodes mess up the analysis? + switch (rng(5)) { + case 0: + return '(a/* ignore */++)'; + case 1: + return '(b/* ignore */--)'; + case 2: + return '(++/* ignore */a)'; + case 3: + return '(--/* ignore */b)'; + case 4: + // only groups that wrap a single variable return a "Reference", so this is still valid. + // may just be a parser edge case that is invisible to uglify... + return '(--(b))'; + default: + return '(--/* ignore */b)'; + } + } +} + +function createTypeofExpr(recurmax) { + if (--recurmax < 0) { + return 'typeof undefined === "undefined"'; + } + + 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 createValue() { + 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)]; +} + +function createAssignment() { + return ASSIGNMENTS[rng(ASSIGNMENTS.length)]; +} + +function createUnaryOp() { + 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 log(ok) { + console.log("//============================================================="); + if (!ok) console.log("// !!!!!! Failed..."); + console.log("// original code"); + console.log("//"); + console.log(original_code); + console.log(); + console.log(); + console.log("//-------------------------------------------------------------"); + console.log("// original code (beautify'd)"); + console.log("//"); + console.log(beautify_code); + console.log(); + console.log(); + console.log("//-------------------------------------------------------------"); + console.log("// uglified code"); + console.log("//"); + console.log(uglify_code); + console.log(); + console.log(); + console.log("original result:"); + console.log(original_result); + console.log("beautified result:"); + console.log(beautify_result); + console.log("uglified result:"); + console.log(uglify_result); + if (!ok) console.log("!!!!!! Failed..."); +} + +var num_iterations = +process.argv[2] || 1/0; +var verbose = process.argv[3] === 'v' || process.argv[2] === 'v'; +var verbose_interval = process.argv[3] === 'V' || process.argv[2] === 'V'; +for (var round = 0; round < num_iterations; round++) { + var parse_error = false; + process.stdout.write(round + " of " + num_iterations + "\r"); + var original_code = [ + "var a = 100, b = 10;", + createFunctionDecls(rng(MAX_GENERATED_FUNCTIONS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH), + "console.log(a, b);" + ].join("\n"); + var original_result = run_code(original_code); + + 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); + + try { + var uglify_code = minify(beautify_code, { + fromString: true, + mangle: true, + compress: { + passes: 3, + }, + output: { + //beautify: true, + //bracketize: true, + }, + }).code; + } catch(e) { + parse_error = 2; + } + var uglify_result = run_code(uglify_code); + + var ok = !parse_error && original_result == beautify_result && original_result == uglify_result; + if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(ok); + if (parse_error === 1) console.log('Parse error while beautifying'); + if (parse_error === 2) console.log('Parse error while uglifying'); + if (!ok) break; +}