diff --git a/README.md b/README.md index 812ac607..8e672b0c 100644 --- a/README.md +++ b/README.md @@ -675,7 +675,13 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `if_return` (default: `true`) -- optimizations for if/return and if/continue -- `inline` (default: `true`) -- embed simple functions +- `inline` (default: `true`) -- inline calls to function with simple/`return` statement: + - `false` -- same as `0` + - `0` -- disabled inlining + - `1` -- inline simple functions + - `2` -- inline functions with arguments + - `3` -- inline functions with arguments and variables + - `true` -- same as `3` - `join_vars` (default: `true`) -- join consecutive `var` statements diff --git a/appveyor.yml b/appveyor.yml index 4d8c2e2b..8972925c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,24 +1,20 @@ environment: matrix: - - nodejs_version: "0.10" - - nodejs_version: "0.12" - - nodejs_version: "4.0" - - nodejs_version: "6.0" - + - nodejs_version: "0.10" + - nodejs_version: "0.12" + - nodejs_version: "4" + - nodejs_version: "6" + - nodejs_version: "8" +install: + - ps: Install-Product node $env:nodejs_version + - set UGLIFYJS_TEST_ALL=1 + - npm install +build: off +cache: + - tmp matrix: fast_finish: true - -platform: - - x86 - - x64 - -install: - - ps: Install-Product node $env:nodejs_version $env:platform - - npm install - test_script: - node --version - npm --version - npm test - -build: off diff --git a/bin/uglifyjs b/bin/uglifyjs index 3ff9b254..cecc98be 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -3,11 +3,7 @@ "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); -}); +require("../tools/exit"); var fs = require("fs"); var info = require("../package.json"); diff --git a/lib/compress.js b/lib/compress.js index c955d2dd..bb25095a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -105,6 +105,7 @@ function Compressor(options, false_by_default) { }); } } + if (this.options["inline"] === true) this.options["inline"] = 3; var pure_funcs = this.options["pure_funcs"]; if (typeof pure_funcs == "function") { this.pure_funcs = pure_funcs; @@ -317,12 +318,13 @@ merge(Compressor.prototype, { def(AST_Node, noop); function reset_def(compressor, def) { + def.assignments = 0; def.direct_access = false; def.escaped = false; if (def.scope.uses_eval || def.scope.uses_with) { def.fixed = false; } else if (def.orig[0] instanceof AST_SymbolConst || !compressor.exposed(def)) { - def.fixed = undefined; + def.fixed = def.init; } else { def.fixed = false; } @@ -332,9 +334,16 @@ merge(Compressor.prototype, { def.single_use = undefined; } - function reset_variables(compressor, node) { + function reset_variables(tw, compressor, node) { node.variables.each(function(def) { reset_def(compressor, def); + if (def.fixed === null) { + def.safe_ids = tw.safe_ids; + mark(tw, def, true); + } else if (def.fixed) { + tw.loop_ids[def.id] = tw.in_loop; + mark(tw, def, true); + } }); } @@ -369,10 +378,16 @@ merge(Compressor.prototype, { } function safe_to_assign(tw, def, value) { + if (def.fixed === undefined) return true; + if (def.fixed === null && def.safe_ids) { + def.safe_ids[def.id] = false; + delete def.safe_ids; + return true; + } if (!HOP(tw.safe_ids, def.id)) return false; if (!safe_to_read(tw, def)) return false; if (def.fixed === false) return false; - if (def.fixed != null && (!value || def.references.length > 0)) return false; + if (def.fixed != null && (!value || def.references.length > def.assignments)) return false; return all(def.orig, function(sym) { return !(sym instanceof AST_SymbolConst || sym instanceof AST_SymbolDefun @@ -489,11 +504,9 @@ merge(Compressor.prototype, { } if (node.operator != "=" || !(node.left instanceof AST_SymbolRef)) return; var d = node.left.definition(); - if (safe_to_assign(tw, d, node.right) - || d.fixed === undefined && all(d.orig, function(sym) { - return sym instanceof AST_SymbolVar; - })) { + if (safe_to_assign(tw, d, node.right)) { d.references.push(node.left); + d.assignments++; d.fixed = function() { return node.right; }; @@ -533,19 +546,10 @@ merge(Compressor.prototype, { }); function mark_def_node(tw, descend, compressor) { - reset_variables(compressor, this); this.inlined = false; - var d = this.name.definition(); - if (compressor.exposed(d) || safe_to_read(tw, d)) { - d.fixed = false; - } else { - d.fixed = this; - d.single_use = ref_once(tw, compressor, d); - tw.loop_ids[d.id] = tw.in_loop; - mark(tw, d, true); - } var save_ids = tw.safe_ids; tw.safe_ids = Object.create(null); + reset_variables(tw, compressor, this); descend(); tw.safe_ids = save_ids; return true; @@ -600,9 +604,9 @@ merge(Compressor.prototype, { function mark_func_expr(tw, descend, compressor) { var node = this; - reset_variables(compressor, node); node.inlined = false; push(tw); + reset_variables(tw, compressor, node); var iife; if (!node.name && (iife = tw.parent()) instanceof AST_Call @@ -693,7 +697,7 @@ merge(Compressor.prototype, { this.globals.each(function(def) { reset_def(compressor, def); }); - reset_variables(compressor, this); + reset_variables(tw, compressor, this); }); def(AST_Try, function(tw, descend, compressor) { reset_block_variables(compressor, this); @@ -715,7 +719,7 @@ merge(Compressor.prototype, { return; } var d = node.name.definition(); - if (d.fixed === undefined || safe_to_assign(tw, d, node.value)) { + if (safe_to_assign(tw, d, node.value)) { if (node.value) { d.fixed = function() { return node.value; @@ -723,8 +727,6 @@ merge(Compressor.prototype, { tw.loop_ids[d.id] = tw.in_loop; mark(tw, d, false); descend(); - } else { - d.fixed = null; } mark(tw, d, true); return true; @@ -957,6 +959,7 @@ merge(Compressor.prototype, { } if (compressor.sequences_limit > 0) { sequencesize(statements, compressor); + sequencesize_2(statements, compressor); } if (compressor.option("join_vars")) { join_consecutive_vars(statements, compressor); @@ -1259,10 +1262,10 @@ merge(Compressor.prototype, { if (!expr.left.has_side_effects(compressor)) { candidates.push(hit_stack.slice()); } - } else if (expr instanceof AST_Unary) { - if (expr.operator == "++" || expr.operator == "--") { - candidates.push(hit_stack.slice()); - } + extract_candidates(expr.right); + } else if (expr instanceof AST_Binary) { + extract_candidates(expr.left); + extract_candidates(expr.right); } else if (expr instanceof AST_Call) { extract_candidates(expr.expression); expr.args.forEach(extract_candidates); @@ -1275,10 +1278,14 @@ merge(Compressor.prototype, { } else if (expr instanceof AST_Definitions && (compressor.option("unused") || !(expr instanceof AST_Const))) { expr.definitions.forEach(extract_candidates); + } else if (expr instanceof AST_DWLoop) { + extract_candidates(expr.condition); } else if (expr instanceof AST_Exit) { if (expr.value) extract_candidates(expr.value); } else if (expr instanceof AST_For) { if (expr.init) extract_candidates(expr.init); + if (expr.condition) extract_candidates(expr.condition); + if (expr.step) extract_candidates(expr.step); } else if (expr instanceof AST_If) { extract_candidates(expr.condition); } else if (expr instanceof AST_Sequence) { @@ -1288,21 +1295,31 @@ merge(Compressor.prototype, { } else if (expr instanceof AST_Switch) { extract_candidates(expr.expression); expr.body.forEach(extract_candidates); + } else if (expr instanceof AST_Unary) { + if (expr.operator == "++" || expr.operator == "--") { + candidates.push(hit_stack.slice()); + } } else if (expr instanceof AST_VarDef) { - if (expr.value) candidates.push(hit_stack.slice()); + if (expr.value) { + candidates.push(hit_stack.slice()); + extract_candidates(expr.value); + } } hit_stack.pop(); } function find_stop(node, level) { var parent = scanner.parent(level); + if (parent instanceof AST_Binary) return node; if (parent instanceof AST_Call) return node; if (parent instanceof AST_Case) return node; if (parent instanceof AST_Conditional) return node; if (parent instanceof AST_Exit) return node; if (parent instanceof AST_If) return node; + if (parent instanceof AST_IterationStatement) return node; if (parent instanceof AST_Sequence) return find_stop(parent, level + 1); if (parent instanceof AST_Switch) return node; + if (parent instanceof AST_VarDef) return node; return null; } @@ -1450,12 +1467,13 @@ merge(Compressor.prototype, { var in_lambda = self instanceof AST_Lambda; for (var i = statements.length; --i >= 0;) { var stat = statements[i]; - var next = statements[i + 1]; + var j = next_index(i); + var next = statements[j]; if (in_lambda && !next && stat instanceof AST_Return) { if (!stat.value) { CHANGED = true; - statements.length--; + statements.splice(i, 1); continue; } if (stat.value instanceof AST_UnaryPrefix && stat.value.operator == "void") { @@ -1525,19 +1543,22 @@ merge(Compressor.prototype, { CHANGED = true; stat = stat.clone(); stat.alternative = next; - statements.splice(i, 2, stat.transform(compressor)); + statements.splice(i, 1, stat.transform(compressor)); + statements.splice(j, 1); continue; } //--- // if (foo()) return x; [ return ; ] ==> return foo() ? x : undefined; - if (multiple_if_returns && in_lambda && value && !stat.alternative - && (!next || next instanceof AST_Return)) { + if (value && !stat.alternative + && (!next && in_lambda && multiple_if_returns + || next instanceof AST_Return)) { CHANGED = true; stat = stat.clone(); stat.alternative = next || make_node(AST_Return, stat, { value: null }); - statements.splice(i, next ? 2 : 1, stat.transform(compressor)); + statements.splice(i, 1, stat.transform(compressor)); + if (next) statements.splice(j, 1); continue; } //--- @@ -1546,10 +1567,10 @@ merge(Compressor.prototype, { // if sequences is not enabled, this can lead to an endless loop (issue #866). // however, with sequences on this helps producing slightly better output for // the example code. - var prev = statements[i - 1]; + var prev = statements[prev_index(i)]; if (compressor.option("sequences") && in_lambda && !stat.alternative && prev instanceof AST_If && prev.body instanceof AST_Return - && i + 2 == statements.length && next instanceof AST_SimpleStatement) { + && next_index(j) == statements.length && next instanceof AST_SimpleStatement) { CHANGED = true; stat = stat.clone(); stat.alternative = make_node(AST_BlockStatement, next, { @@ -1560,7 +1581,8 @@ merge(Compressor.prototype, { }) ] }); - statements.splice(i, 2, stat.transform(compressor)); + statements.splice(i, 1, stat.transform(compressor)); + statements.splice(j, 1); continue; } } @@ -1614,6 +1636,26 @@ merge(Compressor.prototype, { } return body; } + + function next_index(i) { + for (var j = i + 1, len = statements.length; j < len; j++) { + var stat = statements[j]; + if (!(stat instanceof AST_Definitions && declarations_only(stat))) { + break; + } + } + return j; + } + + function prev_index(i) { + for (var j = i; --j >= 0;) { + var stat = statements[j]; + if (!(stat instanceof AST_Definitions && declarations_only(stat))) { + break; + } + } + return j; + } } function eliminate_dead_code(statements, compressor) { @@ -1649,6 +1691,12 @@ merge(Compressor.prototype, { }); } + function declarations_only(node) { + return all(node.definitions, function(var_def) { + return !var_def.value; + }); + } + function sequencesize(statements, compressor) { if (statements.length < 2) return; var seq = [], n = 0; @@ -1665,6 +1713,9 @@ merge(Compressor.prototype, { var body = stat.body; if (seq.length > 0) body = body.drop_side_effect_free(compressor); if (body) merge_sequence(seq, body); + } else if (stat instanceof AST_Definitions && declarations_only(stat) + || stat instanceof AST_Defun) { + statements[n++] = stat; } else { push_seq(); statements[n++] = stat; @@ -1672,18 +1723,34 @@ merge(Compressor.prototype, { } push_seq(); statements.length = n; - sequencesize_2(statements, compressor); - CHANGED = statements.length != len; + if (n != len) CHANGED = true; + } + + function to_simple_statement(block, decls) { + if (!(block instanceof AST_BlockStatement)) return block; + var stat = null; + for (var i = 0, len = block.body.length; i < len; i++) { + var line = block.body[i]; + if (line instanceof AST_Definitions && declarations_only(line)) { + decls.push(line); + } else if (stat) { + return false; + } else { + stat = line; + } + } + return stat; } function sequencesize_2(statements, compressor) { function cons_seq(right) { n--; + CHANGED = true; var left = prev.body; return make_sequence(left, [ left, right ]).transform(compressor); }; var n = 0, prev; - for (var i = 0, len = statements.length; i < len; i++) { + for (var i = 0; i < statements.length; i++) { var stat = statements[i]; if (prev) { if (stat instanceof AST_For && !(stat.init instanceof AST_Definitions)) { @@ -1700,6 +1767,7 @@ merge(Compressor.prototype, { else { stat.init = prev.body; n--; + CHANGED = true; } } } @@ -1719,6 +1787,26 @@ merge(Compressor.prototype, { stat.expression = cons_seq(stat.expression); } } + if (compressor.option("conditionals") && stat instanceof AST_If) { + var decls = []; + var body = to_simple_statement(stat.body, decls); + var alt = to_simple_statement(stat.alternative, decls); + if (body !== false && alt !== false && decls.length > 0) { + var len = decls.length; + decls.push(make_node(AST_If, stat, { + condition: stat.condition, + body: body || make_node(AST_EmptyStatement, stat.body), + alternative: alt + })); + decls.unshift(n, 1); + [].splice.apply(statements, decls); + i += len; + n += len + 1; + prev = null; + CHANGED = true; + continue; + } + } statements[n++] = stat; prev = stat instanceof AST_SimpleStatement ? stat : null; } @@ -1726,30 +1814,43 @@ merge(Compressor.prototype, { } function join_consecutive_vars(statements, compressor) { + var defs; for (var i = 0, j = -1, len = statements.length; i < len; i++) { var stat = statements[i]; var prev = statements[j]; - if (stat instanceof AST_Definitions && prev && prev.TYPE == stat.TYPE) { - prev.definitions = prev.definitions.concat(stat.definitions); - CHANGED = true; - } - else if (stat instanceof AST_For - && prev instanceof AST_Var - && (!stat.init || stat.init.TYPE == prev.TYPE)) { - CHANGED = true; - if (stat.init) { - stat.init.definitions = prev.definitions.concat(stat.init.definitions); + if (stat instanceof AST_Definitions) { + if (prev && prev.TYPE == stat.TYPE) { + prev.definitions = prev.definitions.concat(stat.definitions); + CHANGED = true; + } else if (defs && defs.TYPE == stat.TYPE && declarations_only(stat)) { + defs.definitions = defs.definitions.concat(stat.definitions); + CHANGED = true; } else { - stat.init = prev; + statements[++j] = stat; + defs = stat; } - statements[j] = stat; - } - else { + } else if (stat instanceof AST_For) { + if (prev instanceof AST_Var && (!stat.init || stat.init.TYPE == prev.TYPE)) { + if (stat.init) { + prev.definitions = prev.definitions.concat(stat.init.definitions); + } + stat.init = prev; + statements[j] = stat; + CHANGED = true; + } else if (defs && stat.init && defs.TYPE == stat.init.TYPE && declarations_only(stat.init)) { + defs.definitions = defs.definitions.concat(stat.init.definitions); + stat.init = null; + statements[++j] = stat; + CHANGED = true; + } else { + statements[++j] = stat; + } + } else { statements[++j] = stat; } } statements.length = j + 1; - }; + } } function extract_declarations_from_unreachable_code(compressor, stat, target) { @@ -2792,6 +2893,7 @@ merge(Compressor.prototype, { }; var in_use = []; var in_use_ids = Object.create(null); // avoid expensive linear scans of in_use + var fixed_ids = Object.create(null); if (self instanceof AST_Toplevel && compressor.top_retain) { self.variables.each(function(def) { if (compressor.top_retain(def) && !(def.id in in_use_ids)) { @@ -2847,7 +2949,11 @@ merge(Compressor.prototype, { def.walk(tw); destructuring_value = destructuring_cache; } else { - initializations.add(def.name.definition().id, def.value); + var node_def = def.name.definition();; + initializations.add(node_def.id, def.value); + if (def.name.fixed_value() === def.value) { + fixed_ids[node_def.id] = true; + } } if (def.value.has_side_effects(compressor)) { def.value.walk(tw); @@ -2878,13 +2984,16 @@ merge(Compressor.prototype, { var parent = tt.parent(); if (drop_vars) { var sym = assign_as_unused(node); - if (sym instanceof AST_SymbolRef - && !((sym = sym.definition()).id in in_use_ids) - && (drop_vars || !sym.global)) { + if (sym instanceof AST_SymbolRef) { + var def = sym.definition(); + var in_use = def.id in in_use_ids; if (node instanceof AST_Assign) { - return maintain_this_binding(parent, node, node.right.transform(tt)); - } - return make_node(AST_Number, node, { + if (!in_use + || def.id in fixed_ids + && node.left.fixed_value() !== node.right) { + return maintain_this_binding(parent, node, node.right.transform(tt)); + } + } else if (!in_use) return make_node(AST_Number, node, { value: 0 }); } @@ -2959,13 +3068,16 @@ merge(Compressor.prototype, { operator: "=", left: make_node(AST_SymbolRef, def.name, def.name), right: def.value - })); + }).transform(tt)); } remove(var_defs, def); sym.eliminated++; return; } } + if (def.value && sym.id in fixed_ids && def.name.fixed_value() !== def.value) { + def.value = def.value.drop_side_effect_free(compressor); + } if (def.value) { if (side_effects.length > 0) { if (tail.length > 0) { @@ -3077,15 +3189,20 @@ merge(Compressor.prototype, { self.transform(tt); function scan_ref_scoped(node, descend) { - var sym; - if ((sym = assign_as_unused(node)) instanceof AST_SymbolRef + var node_def, sym = assign_as_unused(node); + if (sym instanceof AST_SymbolRef && !is_ref_of(node.left, AST_SymbolBlockDeclaration) - && self.variables.get(sym.name) === sym.definition()) { - if (node instanceof AST_Assign) node.right.walk(tw); + && self.variables.get(sym.name) === (node_def = sym.definition())) { + if (node instanceof AST_Assign) { + node.right.walk(tw); + if (node.left.fixed_value() === node.right) { + fixed_ids[node_def.id] = true; + } + } return true; } if (node instanceof AST_SymbolRef) { - var node_def = node.definition(); + node_def = node.definition(); if (!(node_def.id in in_use_ids)) { in_use_ids[node_def.id] = true; in_use.push(node_def); @@ -3382,7 +3499,12 @@ merge(Compressor.prototype, { }); def(AST_Assign, function(compressor){ var left = this.left; - if (left.has_side_effects(compressor)) return this; + if (left.has_side_effects(compressor) + || compressor.has_directive("use strict") + && left instanceof AST_PropAccess + && left.expression.is_constant()) { + return this; + } this.write_only = true; while (left instanceof AST_PropAccess) { left = left.expression; @@ -4018,6 +4140,27 @@ merge(Compressor.prototype, { operator: "!" }).optimize(compressor); break; + case "RegExp": + var params = []; + if (all(self.args, function(arg) { + var value = arg.evaluate(compressor); + params.unshift(value); + return arg !== value; + })) { + try { + return best_of(compressor, self, make_node(AST_RegExp, self, { + value: RegExp.apply(RegExp, params), + })); + } catch (ex) { + compressor.warn("Error converting {expr} [{file}:{line},{col}]", { + expr: self.print_to_string(), + file: self.start.file, + line: self.start.line, + col: self.start.col + }); + } + } + break; case "Symbol": // Symbol's argument is only used for debugging. self.args = []; @@ -4219,24 +4362,22 @@ merge(Compressor.prototype, { } } if (is_func && !fn.is_generator && !fn.async) { - var def, value, scope, level = -1; + var def, value, scope, in_loop, level = -1; if (compressor.option("inline") && simple_args && !fn.uses_arguments && !fn.uses_eval - && (fn.body instanceof AST_Node || fn.body.length == 1) - && (exp === fn ? !fn.name - : compressor.option("unused") + && !(fn.name && fn instanceof AST_Function) + && (value = can_flatten_body(stat)) + && (exp === fn + || compressor.option("unused") && (def = exp.definition()).references.length == 1 && !recursive_ref(compressor, def) && fn.is_constant_expression(exp.scope)) && !self.pure && !fn.contains_this() - && can_flatten_args(fn) - && (value = flatten_body(stat))) { - var expressions = flatten_args(fn); - expressions.push(value.clone(true)); - return make_sequence(self, expressions).optimize(compressor); + && can_inject_symbols()) { + return make_sequence(self, flatten_fn()).optimize(compressor); } if (compressor.option("side_effects") && !(fn.body instanceof AST_Node) && all(fn.body, is_empty)) { var args = self.args.concat(make_node(AST_Undefined, self)); @@ -4266,19 +4407,45 @@ merge(Compressor.prototype, { } return self; - function can_flatten_args(fn) { - var catches = Object.create(null), defs; - do { - scope = compressor.parent(++level); - if (scope instanceof AST_Catch) { - catches[scope.argname.name] = true; - } else if (scope instanceof AST_IterationStatement) { - defs = []; - } else if (scope instanceof AST_SymbolRef) { - if (scope.fixed_value() instanceof AST_Scope) return false; + function return_value(stat) { + if (!stat) return make_node(AST_Undefined, self); + if (stat instanceof AST_Return) { + if (!stat.value) return make_node(AST_Undefined, self); + return stat.value.clone(true); + } + if (stat instanceof AST_SimpleStatement) { + return make_node(AST_UnaryPrefix, stat, { + operator: "void", + expression: stat.body.clone(true) + }); + } + } + + function can_flatten_body(stat) { + var body = fn.body instanceof AST_Node ? [ fn.body ] : fn.body; + var len = body.length; + if (compressor.option("inline") < 3) { + return len == 1 && return_value(stat); + } + stat = null; + for (var i = 0; i < len; i++) { + var line = body[i]; + if (line instanceof AST_Var) { + if (stat && !all(line.definitions, function(var_def) { + return !var_def.value; + })) { + return false; + } + } else if (stat) { + return false; + } else { + stat = line; } - } while (!(scope instanceof AST_Scope) || scope instanceof AST_Arrow); - var safe_to_inject = compressor.toplevel.vars || !(scope instanceof AST_Toplevel); + } + return return_value(stat); + } + + function can_inject_args(catches, safe_to_inject) { for (var i = 0, len = fn.argnames.length; i < len; i++) { var arg = fn.argnames[i]; if (arg instanceof AST_DefaultAssign) { @@ -4297,44 +4464,119 @@ merge(Compressor.prototype, { || scope.var_names()[arg.name]) { return false; } - if (defs) defs.push(arg.definition()); + if (in_loop) in_loop.push(arg.definition()); } - return !defs || defs.length == 0 || !is_reachable(stat, defs); + return true; } - function flatten_args(fn) { - var decls = []; - var expressions = []; - for (var len = fn.argnames.length, i = len; --i >= 0;) { - var name = fn.argnames[i]; - var value = self.args[i]; - if (name.__unused || !name.name) { - if (value || expressions.length) { - expressions.unshift(value || make_node(AST_Undefined, self)); + function can_inject_vars(catches, safe_to_inject) { + var len = fn.body.length; + for (var i = 0; i < len; i++) { + var stat = fn.body[i]; + if (!(stat instanceof AST_Var)) continue; + if (!safe_to_inject) return false; + for (var j = stat.definitions.length; --j >= 0;) { + var name = stat.definitions[j].name; + if (catches[name.name] + || identifier_atom(name.name) + || scope.var_names()[name.name]) { + return false; } - } else { - var def = name.definition(); - scope.var_names()[name.name] = true; - scope.variables.set(name.name, def); - scope.enclosed.push(def); - var symbol = make_node(AST_SymbolVar, name, name); - def.orig.push(symbol); - decls.unshift(make_node(AST_VarDef, name, { - name: symbol, - value: null - })); - var sym = make_node(AST_SymbolRef, name, name); - def.references.push(sym); - expressions.unshift(make_node(AST_Assign, self, { - operator: "=", - left: sym, - right: value || make_node(AST_Undefined, self) - })); + if (in_loop) in_loop.push(name.definition()); } } - for (i = len, len = self.args.length; i < len; i++) { + return true; + } + + function can_inject_symbols() { + var catches = Object.create(null); + do { + scope = compressor.parent(++level); + if (scope instanceof AST_Catch) { + catches[scope.argname.name] = true; + } else if (scope instanceof AST_IterationStatement) { + in_loop = []; + } else if (scope instanceof AST_SymbolRef) { + if (scope.fixed_value() instanceof AST_Scope) return false; + } + } while (!(scope instanceof AST_Scope) || scope instanceof AST_Arrow); + var safe_to_inject = !(scope instanceof AST_Toplevel) || compressor.toplevel.vars; + var inline = compressor.option("inline"); + if (!can_inject_vars(catches, inline >= 3 && safe_to_inject)) return false; + if (!can_inject_args(catches, inline >= 2 && safe_to_inject)) return false; + return !in_loop || in_loop.length == 0 || !is_reachable(fn, in_loop); + } + + function append_var(decls, expressions, name, value) { + var def = name.definition(); + scope.variables.set(name.name, def); + scope.enclosed.push(def); + if (!scope.var_names()[name.name]) { + scope.var_names()[name.name] = true; + decls.push(make_node(AST_VarDef, name, { + name: name, + value: null + })); + } + var sym = make_node(AST_SymbolRef, name, name); + def.references.push(sym); + if (value) expressions.push(make_node(AST_Assign, self, { + operator: "=", + left: sym, + right: value + })); + } + + function flatten_args(decls, expressions) { + var len = fn.argnames.length; + for (var i = self.args.length; --i >= len;) { expressions.push(self.args[i]); } + for (i = len; --i >= 0;) { + var name = fn.argnames[i]; + var value = self.args[i]; + if (name.__unused || !name.name || scope.var_names()[name.name]) { + if (value) expressions.push(value); + } else { + var symbol = make_node(AST_SymbolVar, name, name); + name.definition().orig.push(symbol); + if (!value && in_loop) value = make_node(AST_Undefined, self); + append_var(decls, expressions, symbol, value); + } + } + decls.reverse(); + expressions.reverse(); + } + + function flatten_vars(decls, expressions) { + var pos = expressions.length; + for (var i = 0, lines = fn.body.length; i < lines; i++) { + var stat = fn.body[i]; + if (!(stat instanceof AST_Var)) continue; + for (var j = 0, defs = stat.definitions.length; j < defs; j++) { + var var_def = stat.definitions[j]; + var name = var_def.name; + append_var(decls, expressions, name, var_def.value); + if (in_loop) { + var def = name.definition(); + var sym = make_node(AST_SymbolRef, name, name); + def.references.push(sym); + expressions.splice(pos++, 0, make_node(AST_Assign, var_def, { + operator: "=", + left: sym, + right: make_node(AST_Undefined, name) + })); + } + } + } + } + + function flatten_fn() { + var decls = []; + var expressions = []; + flatten_args(decls, expressions); + flatten_vars(decls, expressions); + expressions.push(value); if (decls.length) { i = scope.body.indexOf(compressor.parent(level - 1)) + 1; scope.body.splice(i, 0, make_node(AST_Var, fn, { @@ -4343,17 +4585,6 @@ merge(Compressor.prototype, { } return expressions; } - - function flatten_body(stat) { - if (stat instanceof AST_Return) { - return stat.value; - } else if (stat instanceof AST_SimpleStatement) { - return make_node(AST_UnaryPrefix, stat, { - operator: "void", - expression: stat.body - }); - } - } }); OPT(AST_New, function(self, compressor){ @@ -5159,9 +5390,8 @@ merge(Compressor.prototype, { if (reachable) return true; if (node instanceof AST_Scope && node !== self) { var parent = scan_scope.parent(); - if (!(parent instanceof AST_Call && parent.expression === node)) { - node.walk(find_ref); - } + if (parent instanceof AST_Call && parent.expression === node) return; + node.walk(find_ref); return true; } }); @@ -5595,6 +5825,14 @@ merge(Compressor.prototype, { }); OPT(AST_Dot, function(self, compressor){ + if (self.property == "arguments" || self.property == "caller") { + compressor.warn("Function.protoype.{prop} not supported [{file}:{line},{col}]", { + prop: self.property, + file: self.start.file, + line: self.start.line, + col: self.start.col + }); + } var def = self.resolve_defines(compressor); if (def) { return def.optimize(compressor); @@ -5609,11 +5847,27 @@ merge(Compressor.prototype, { elements: [] }); break; + case "Function": + self.expression = make_node(AST_Function, self.expression, { + argnames: [], + body: [] + }); + break; + case "Number": + self.expression = make_node(AST_Number, self.expression, { + value: 0 + }); + break; case "Object": self.expression = make_node(AST_Object, self.expression, { properties: [] }); break; + case "RegExp": + self.expression = make_node(AST_RegExp, self.expression, { + value: /t/ + }); + break; case "String": self.expression = make_node(AST_String, self.expression, { value: "" diff --git a/lib/minify.js b/lib/minify.js index 4a6d120b..874f6437 100644 --- a/lib/minify.js +++ b/lib/minify.js @@ -29,7 +29,6 @@ function set_shorthand(name, options, keys) { function init_cache(cache) { if (!cache) return; - if (!("cname" in cache)) cache.cname = -1; if (!("props" in cache)) { cache.props = new Dictionary(); } else if (!(cache.props instanceof Dictionary)) { @@ -39,7 +38,6 @@ function init_cache(cache) { function to_json(cache) { return { - cname: cache.cname, props: cache.props.toObject() }; } diff --git a/lib/output.js b/lib/output.js index e43cbc9a..b2353a4c 100644 --- a/lib/output.js +++ b/lib/output.js @@ -557,7 +557,11 @@ function OutputStream(options) { var token = node.end; if (!token) return; var comments = token[tail ? "comments_before" : "comments_after"]; - if (comments && comments._dumped !== self) { + if (comments + && comments._dumped !== self + && (node instanceof AST_Statement || all(comments, function(c) { + return !/comment[134]/.test(c.type); + }))) { comments._dumped = self; var insert = OUTPUT.length; comments.filter(comment_filter, node).forEach(function(c, i) { @@ -627,7 +631,7 @@ function OutputStream(options) { add_mapping : add_mapping, option : function(opt) { return options[opt] }, prepend_comments: readonly ? noop : prepend_comments, - append_comments : readonly ? noop : append_comments, + append_comments : readonly || comment_filter === return_false ? noop : append_comments, line : function() { return current_line }, col : function() { return current_col }, pos : function() { return current_pos }, diff --git a/lib/parse.js b/lib/parse.js index fb381ec3..b32292cb 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1918,12 +1918,14 @@ function parse($TEXT, options) { } else { args = []; } - return subscripts(new AST_New({ + var call = new AST_New({ start : start, expression : newexp, args : args, end : prev() - }), allow_calls); + }); + mark_pure(call); + return subscripts(call, allow_calls); }; function as_atom_node() { diff --git a/lib/propmangle.js b/lib/propmangle.js index fb9e68e9..b7e12905 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -130,12 +130,15 @@ function mangle_properties(ast, options) { if (!Array.isArray(reserved)) reserved = []; if (!options.builtins) find_builtins(reserved); - var cache = options.cache; - if (cache == null) { - cache = { - cname: -1, - props: new Dictionary() - }; + var cname = -1; + var cache; + if (options.cache) { + cache = options.cache.props; + cache.each(function(mangled_name) { + push_uniq(reserved, mangled_name); + }); + } else { + cache = new Dictionary(); } var regex = options.regex; @@ -192,7 +195,7 @@ function mangle_properties(ast, options) { if (unmangleable.indexOf(name) >= 0) return false; if (reserved.indexOf(name) >= 0) return false; if (options.only_cache) { - return cache.props.has(name); + return cache.has(name); } if (/^-?[0-9]+(\.[0-9]+)?(e[+-][0-9]+)?$/.test(name)) return false; return true; @@ -201,7 +204,7 @@ function mangle_properties(ast, options) { function should_mangle(name) { if (regex && !regex.test(name)) return false; if (reserved.indexOf(name) >= 0) return false; - return cache.props.has(name) + return cache.has(name) || names_to_mangle.indexOf(name) >= 0; } @@ -219,7 +222,7 @@ function mangle_properties(ast, options) { return name; } - var mangled = cache.props.get(name); + var mangled = cache.get(name); if (!mangled) { if (debug) { // debug mode: use a prefix and suffix to preserve readability, e.g. o.foo -> o._$foo$NNN_. @@ -233,11 +236,11 @@ function mangle_properties(ast, options) { // either debug mode is off, or it is on and we could not use the mangled name if (!mangled) { do { - mangled = base54(++cache.cname); + mangled = base54(++cname); } while (!can_mangle(mangled)); } - cache.props.set(name, mangled); + cache.set(name, mangled); } return mangled; } diff --git a/lib/scope.js b/lib/scope.js index 1a3d39f9..b0db4308 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -43,9 +43,10 @@ "use strict"; -function SymbolDef(scope, orig) { +function SymbolDef(scope, orig, init) { this.name = orig.name; this.orig = [ orig ]; + this.init = init; this.eliminated = 0; this.scope = scope; this.references = []; @@ -175,7 +176,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.references = []; } if (node instanceof AST_SymbolLambda) { - defun.def_function(node); + defun.def_function(node, node.name == "arguments" ? undefined : defun); } else if (node instanceof AST_SymbolDefun) { // Careful here, the scope where this should be defined is @@ -183,10 +184,10 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // scope when we encounter the AST_Defun node (which is // instanceof AST_Scope) but we get to the symbol a bit // later. - mark_export((node.scope = defun.parent_scope.get_defun_scope()).def_function(node), 1); + mark_export((node.scope = defun.parent_scope.get_defun_scope()).def_function(node, defun), 1); } else if (node instanceof AST_SymbolClass) { - mark_export(defun.def_variable(node), 1); + mark_export(defun.def_variable(node, defun), 1); } else if (node instanceof AST_SymbolImport) { scope.def_variable(node); @@ -194,12 +195,17 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ else if (node instanceof AST_SymbolDefClass) { // This deals with the name of the class being available // inside the class. - mark_export((node.scope = defun.parent_scope).def_function(node), 1); + mark_export((node.scope = defun.parent_scope).def_function(node, defun), 1); } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolLet || node instanceof AST_SymbolConst) { - var def = ((node instanceof AST_SymbolBlockDeclaration) ? scope : defun).def_variable(node); + var def; + if (node instanceof AST_SymbolBlockDeclaration) { + def = scope.def_variable(node); + } else { + def = defun.def_variable(node, node.TYPE == "SymbolVar" ? null : undefined); + } if (!all(def.orig, function(sym) { if (sym === node) return true; if (node instanceof AST_SymbolBlockDeclaration) { @@ -334,10 +340,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ }); } } - - if (options.cache) { - this.cname = options.cache.cname; - } }); AST_Toplevel.DEFMETHOD("def_global", function(node){ @@ -407,29 +409,32 @@ AST_Scope.DEFMETHOD("find_variable", function(name){ || (this.parent_scope && this.parent_scope.find_variable(name)); }); -AST_Scope.DEFMETHOD("def_function", function(symbol){ - var def = this.def_variable(symbol); +AST_Scope.DEFMETHOD("def_function", function(symbol, init){ + var def = this.def_variable(symbol, init); + if (!def.init) def.init = init; this.functions.set(symbol.name, def); return def; }); -AST_Scope.DEFMETHOD("def_variable", function(symbol){ - var def; - if (!this.variables.has(symbol.name)) { - def = new SymbolDef(this, symbol); +AST_Scope.DEFMETHOD("def_variable", function(symbol, init){ + var def = this.variables.get(symbol.name); + if (def) { + def.orig.push(symbol); + if (def.init && (def.scope !== symbol.scope || def.init instanceof AST_Function)) { + def.init = init; + } + } else { + def = new SymbolDef(this, symbol, init); this.variables.set(symbol.name, def); def.global = !this.parent_scope; - } else { - def = this.variables.get(symbol.name); - def.orig.push(symbol); } return symbol.thedef = def; }); -AST_Scope.DEFMETHOD("next_mangled", function(options){ - var ext = this.enclosed; +function next_mangled(scope, options) { + var ext = scope.enclosed; out: while (true) { - var m = base54(++this.cname); + var m = base54(++scope.cname); if (!is_identifier(m)) continue; // skip over "do" // https://github.com/mishoo/UglifyJS2/issues/242 -- do not @@ -446,6 +451,18 @@ AST_Scope.DEFMETHOD("next_mangled", function(options){ } return m; } +} + +AST_Scope.DEFMETHOD("next_mangled", function(options){ + return next_mangled(this, options); +}); + +AST_Toplevel.DEFMETHOD("next_mangled", function(options){ + var name; + do { + name = next_mangled(this, options); + } while (member(name, this.mangled_names)); + return name; }); AST_Function.DEFMETHOD("next_mangled", function(options, def){ @@ -459,7 +476,7 @@ AST_Function.DEFMETHOD("next_mangled", function(options, def){ var tricky_name = tricky_def ? tricky_def.mangled_name || tricky_def.name : null; while (true) { - var name = AST_Lambda.prototype.next_mangled.call(this, options, def); + var name = next_mangled(this, options); if (!tricky_name || tricky_name != name) return name; } @@ -511,8 +528,14 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ var lname = -1; var to_mangle = []; + var mangled_names = this.mangled_names = []; if (options.cache) { this.globals.each(collect); + if (options.cache.props) { + options.cache.props.each(function(mangled_name) { + push_uniq(mangled_names, mangled_name); + }); + } } var tw = new TreeWalker(function(node, descend){ @@ -546,10 +569,6 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ def.mangle(options); }); - if (options.cache) { - options.cache.cname = this.cname; - } - function collect(symbol) { if (!member(symbol.name, options.reserved)) { to_mangle.push(symbol); diff --git a/package.json b/package.json index a64c8039..1902590e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "https://github.com/mishoo/UglifyJS2/tree/harmony", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "3.3.4", + "version": "3.3.5", "engines": { "node": ">=0.8.0" }, diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 4d61d39f..7838fdbb 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -1203,3 +1203,24 @@ issue_2560: { "2", ] } + +hoist_decl: { + options = { + conditionals: true, + join_vars: true, + sequences: true, + } + input: { + if (x()) { + var a; + y(); + } else { + z(); + var b; + } + } + expect: { + var a, b; + x() ? y() : z(); + } +} diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index f571c360..eb213b95 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -1100,3 +1100,31 @@ issue_2692: { } expect_stdout: "function" } + +issue_2701: { + options = { + dead_code: true, + inline: false, + } + input: { + function f(a) { + return a = function() { + return function() { + return a; + }; + }(); + } + console.log(typeof f()()); + } + expect: { + function f(a) { + return a = function() { + return function() { + return a; + }; + }(); + } + console.log(typeof f()()); + } + expect_stdout: "function" +} diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index fda27a38..66b9d5ff 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -1895,3 +1895,125 @@ issue_2665: { } expect_stdout: "-1" } + +double_assign_1: { + options = { + passes: 2, + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f1() { + var a = {}; + var a = []; + return a; + } + function f2() { + var a = {}; + a = []; + return a; + } + function f3() { + a = {}; + var a = []; + return a; + } + function f4(a) { + a = {}; + a = []; + return a; + } + function f5(a) { + var a = {}; + a = []; + return a; + } + function f6(a) { + a = {}; + var a = []; + return a; + } + console.log(f1(), f2(), f3(), f4(), f5(), f6()); + } + expect: { + function f1() { + return []; + } + function f2() { + var a; + a = []; + return a; + } + function f3() { + return []; + } + function f4(a) { + a = []; + return a; + } + function f5(a) { + a = []; + return a; + } + function f6(a) { + a = []; + return a; + } + console.log(f1(), f2(), f3(), f4(), f5(), f6()); + } + expect_stdout: true +} + +double_assign_2: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + for (var i = 0; i < 2; i++) + a = void 0, a = {}, console.log(a); + var a; + } + expect: { + for (var i = 0; i < 2; i++) + void 0, a = {}, console.log(a); + var a; + } +} + +double_assign_3: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + for (var i = 0; i < 2; i++) + a = void 0, a = { a: a }, console.log(a); + var a; + } + expect: { + for (var i = 0; i < 2; i++) + a = void 0, a = { a: a }, console.log(a); + var a; + } +} + +cascade_drop_assign: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var a, b = a = "PASS"; + console.log(b); + } + expect: { + var b = "PASS"; + console.log(b); + } + expect_stdout: "PASS" +} diff --git a/test/compress/functions.js b/test/compress/functions.js index e8f1ea4c..18a23378 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -800,12 +800,12 @@ issue_2601_1: { expect: { var a = "FAIL"; (function() { + var b; b = "foo", function(b) { b && b(); }(), b && (a = "PASS"); - var b; })(), console.log(a); } @@ -1318,7 +1318,7 @@ issue_2630_1: { expect: { var c = 0; (function() { - while (c++, void (c = 1 + c)); + while (void (c = 1 + ++c)); })(), console.log(c); } @@ -1332,6 +1332,7 @@ issue_2630_2: { passes: 2, reduce_vars: true, sequences: true, + side_effects: true, unused: true, } input: { @@ -1349,7 +1350,7 @@ issue_2630_2: { expect: { var c = 0; !function() { - while (c += 1, void (c = 1 + c)); + while (void (c = 1 + (c += 1))); }(), console.log(c); } expect_stdout: "2" @@ -1449,7 +1450,7 @@ issue_2630_5: { !function() { do { c *= 10; - } while (c += 3, (c = 2 + c) < 100); + } while ((c = 2 + (c += 3)) < 100); }(); console.log(c); } @@ -1771,3 +1772,347 @@ issue_2663_3: { "reset", ] } + +duplicate_argnames: { + options = { + inline: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a = "PASS"; + function f(b, b, b) { + b && (a = "FAIL"); + } + f(0, console); + console.log(a); + } + expect: { + var a = "PASS"; + console, b && (a = "FAIL"); + var b; + console.log(a); + } + expect_stdout: "PASS" +} + +loop_init_arg: { + options = { + inline: true, + side_effects: true, + toplevel: true, + } + input: { + var a = "PASS"; + for (var k in "12") (function (b) { + (b >>= 1) && (a = "FAIL"), b = 2; + })(); + console.log(a); + } + expect: { + var a = "PASS"; + for (var k in "12") + b = void 0, (b >>= 1) && (a = "FAIL"), b = 2; + var b; + console.log(a); + } + expect_stdout: "PASS" +} + +inline_false: { + options = { + inline: false, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_0: { + options = { + inline: 0, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_1: { + options = { + inline: 1, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + console.log(1); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_2: { + options = { + inline: 2, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + console.log(1); + a = 2, console.log(a); + var a; + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_3: { + options = { + inline: 3, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + console.log(1); + a = 2, console.log(a); + var a; + b = 3, c = b, console.log(c); + var b, c; + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +inline_true: { + options = { + inline: true, + side_effects: true, + toplevel: true, + } + input: { + (function() { + console.log(1); + })(); + (function(a) { + console.log(a); + })(2); + (function(b) { + var c = b; + console.log(c); + })(3); + } + expect: { + console.log(1); + a = 2, console.log(a); + var a; + b = 3, c = b, console.log(c); + var b, c; + } + expect_stdout: [ + "1", + "2", + "3", + ] +} + +use_before_init_in_loop: { + options = { + inline: true, + side_effects: true, + toplevel: true, + } + input: { + var a = "PASS"; + for (var b = 2; --b >= 0;) (function() { + var c = function() { + return 1; + }(c && (a = "FAIL")); + })(); + console.log(a); + } + expect: { + var a = "PASS"; + for (var b = 2; --b >= 0;) + c = void 0, c = (c && (a = "FAIL"), 1); + var c; + console.log(a); + } + expect_stdout: "PASS" +} + +duplicate_arg_var: { + options = { + inline: true, + toplevel: true, + } + input: { + console.log(function(b) { + return b; + var b; + }("PASS")); + } + expect: { + console.log((b = "PASS", b)); + var b; + } + expect_stdout: "PASS" +} + +issue_2737_1: { + options = { + inline: true, + reduce_vars: true, + unused: true, + } + input: { + (function(a) { + while (a()); + })(function f() { + console.log(typeof f); + }); + } + expect: { + (function(a) { + while (a()); + })(function f() { + console.log(typeof f); + }); + } + expect_stdout: "function" +} + +issue_2737_2: { + options = { + inline: true, + reduce_vars: true, + unused: true, + } + input: { + (function(bar) { + for (;bar(); ) break; + })(function qux() { + return console.log("PASS"), qux; + }); + } + expect: { + (function(bar) { + for (;bar(); ) break; + })(function qux() { + return console.log("PASS"), qux; + }); + } + expect_stdout: "PASS" +} diff --git a/test/compress/if_return.js b/test/compress/if_return.js index bc958e68..e8f5b9d4 100644 --- a/test/compress/if_return.js +++ b/test/compress/if_return.js @@ -384,3 +384,73 @@ issue_1317_strict: { expect_stdout: "1" node_version: ">=4" } + +if_var_return: { + options = { + conditionals: true, + if_return: true, + join_vars: true, + sequences: true, + } + input: { + function f() { + var a; + return; + var b; + } + function g() { + var a; + if (u()) { + var b; + return v(); + var c; + } + var d; + if (w()) { + var e; + return x(); + var f; + } else { + var g; + y(); + var h; + } + var i; + z(); + var j; + } + } + expect: { + function f() { + var a, b; + } + function g() { + var a, b, c, d, e, f, g, h, i, j; + return u() ? v() : w() ? x() : (y(), z(), void 0); + } + } +} + +if_if_return_return: { + options = { + conditionals: true, + if_return: true, + } + input: { + function f(a, b) { + if (a) { + if (b) + return b; + return; + } + g(); + } + } + expect: { + function f(a, b) { + if (a) + return b || void 0; + g(); + } + } +} diff --git a/test/compress/issue-269.js b/test/compress/issue-269.js index 1d41dea6..a29e7541 100644 --- a/test/compress/issue-269.js +++ b/test/compress/issue-269.js @@ -64,3 +64,27 @@ strings_concat: { ); } } + +regexp: { + options = { + evaluate: true, + unsafe: true, + } + input: { + RegExp("foo"); + RegExp("bar", "ig"); + RegExp(foo); + RegExp("bar", ig); + RegExp("should", "fail"); + } + expect: { + /foo/; + /bar/ig; + RegExp(foo); + RegExp("bar", ig); + RegExp("should", "fail"); + } + expect_warnings: [ + 'WARN: Error converting RegExp("should","fail") [test/compress/issue-269.js:78,2]', + ] +} diff --git a/test/compress/issue-2719.js b/test/compress/issue-2719.js new file mode 100644 index 00000000..93e51d7c --- /dev/null +++ b/test/compress/issue-2719.js @@ -0,0 +1,32 @@ +warn: { + options = { + evaluate: true, + inline: true, + passes: 2, + properties: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + return g(); + } + function g() { + return g["call" + "er"].arguments; + } + // 3 + console.log(f(1, 2, 3).length); + } + expect: { + // TypeError: Cannot read property 'arguments' of null + console.log(function g() { + return g.caller.arguments; + }().length); + } + expect_warnings: [ + "WARN: Function.protoype.caller not supported [test/compress/issue-2719.js:17,19]", + "WARN: Function.protoype.arguments not supported [test/compress/issue-2719.js:17,19]", + ] +} diff --git a/test/compress/properties.js b/test/compress/properties.js index 0dcc28e8..0d50cdc3 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -569,12 +569,18 @@ native_prototype: { } input: { Array.prototype.splice.apply(a, [1, 2, b, c]); + Function.prototype.call.apply(console.log, console, [ "foo" ]); + Number.prototype.toFixed.call(Math.PI, 2); Object.prototype.hasOwnProperty.call(d, "foo"); + RegExp.prototype.test.call(/foo/, "bar"); String.prototype.indexOf.call(e, "bar"); } expect: { [].splice.apply(a, [1, 2, b, c]); + (function() {}).call.apply(console.log, console, [ "foo" ]); + 0..toFixed.call(Math.PI, 2); ({}).hasOwnProperty.call(d, "foo"); + /t/.test.call(/foo/, "bar"); "".indexOf.call(e, "bar"); } } diff --git a/test/compress/pure_funcs.js b/test/compress/pure_funcs.js index d15bcca3..0df51e5f 100644 --- a/test/compress/pure_funcs.js +++ b/test/compress/pure_funcs.js @@ -414,3 +414,124 @@ issue_2638: { "/* */(a()||b())(c(),d());", ] } + +issue_2705_1: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/ new a(); + /*@__PURE__*/ (new b()); + new (/*@__PURE__*/ c)(); + (/*@__PURE__*/ new d()); + } + expect_exact: [ + "new/* */c;", + ] +} + +issue_2705_2: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/ new a(1)(2)(3); + /*@__PURE__*/ new (b(1))(2)(3); + /*@__PURE__*/ new (c(1)(2))(3); + /*@__PURE__*/ new (d(1)(2)(3)); + new (/*@__PURE__*/ e)(1)(2)(3); + (/*@__PURE__*/ new f(1))(2)(3); + (/*@__PURE__*/ new g(1)(2))(3); + (/*@__PURE__*/ new h(1)(2)(3)); + } + expect_exact: [ + "new/* */e(1)(2)(3);", + "/* */new f(1)(2)(3);", + "/* */new g(1)(2)(3);", + ] +} + +issue_2705_3: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/ new a.x(1).y(2).z(3); + /*@__PURE__*/ new (b.x)(1).y(2).z(3); + /*@__PURE__*/ new (c.x(1)).y(2).z(3); + /*@__PURE__*/ new (d.x(1).y)(2).z(3); + /*@__PURE__*/ new (e.x(1).y(2)).z(3); + /*@__PURE__*/ new (f.x(1).y(2).z)(3); + /*@__PURE__*/ new (g.x(1).y(2).z(3)); + new (/*@__PURE__*/ h).x(1).y(2).z(3); + /* */ new (/*@__PURE__*/ i.x)(1).y(2).z(3); + (/*@__PURE__*/ new j.x(1)).y(2).z(3); + (/*@__PURE__*/ new k.x(1).y)(2).z(3); + (/*@__PURE__*/ new l.x(1).y(2)).z(3); + (/*@__PURE__*/ new m.x(1).y(2).z)(3); + (/*@__PURE__*/ new n.x(1).y(2).z(3)); + } + expect_exact: [ + "new/* */h.x(1).y(2).z(3);", + "/* */new/* */i.x(1).y(2).z(3);", + "/* */new j.x(1).y(2).z(3);", + "/* */new k.x(1).y(2).z(3);", + "/* */new l.x(1).y(2).z(3);", + "/* */new m.x(1).y(2).z(3);", + ] +} + +issue_2705_4: { + options = { + side_effects: true, + } + input: { + (/*@__PURE__*/ new x(), y()); + (w(), /*@__PURE__*/ new x(), y()); + } + expect: { + y(); + w(), y(); + } +} + +issue_2705_5: { + options = { + side_effects: true, + } + input: { + [ /*@__PURE__*/ new x() ]; + [ /*@__PURE__*/ new x(), y() ]; + [ w(), /*@__PURE__*/ new x(), y() ]; + } + expect: { + y(); + w(), y(); + } +} + +issue_2705_6: { + options = { + side_effects: true, + } + beautify = { + comments: "all", + } + input: { + /*@__PURE__*/new (g() || h())(x(), y()); + /* */ new (/*@__PURE__*/ (a() || b()))(c(), d()); + } + expect_exact: [ + "/* */x(),y();", + "/* */new(/* */a()||b())(c(),d());", + ] +} diff --git a/test/compress/pure_getters.js b/test/compress/pure_getters.js index 43c37070..55fce16e 100644 --- a/test/compress/pure_getters.js +++ b/test/compress/pure_getters.js @@ -348,6 +348,57 @@ set_immutable_4: { expect_stdout: true } +set_immutable_5: { + options = { + collapse_vars: true, + conditionals: true, + evaluate: true, + pure_getters: "strict", + reduce_funcs: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + "use strict"; + var a = 1; + a.foo += ""; + if (a.foo) console.log("FAIL"); + else console.log("PASS"); + } + expect: { + "use strict"; + 1..foo += ""; + 1..foo ? console.log("FAIL") : console.log("PASS"); + } + expect_stdout: true +} + +set_immutable_6: { + options = { + collapse_vars: true, + conditionals: true, + evaluate: true, + pure_getters: "strict", + reduce_funcs: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a = 1; + a.foo += ""; + if (a.foo) console.log("FAIL"); + else console.log("PASS"); + } + expect: { + 1..foo ? console.log("FAIL") : console.log("PASS"); + } + expect_stdout: true +} + set_mutable_1: { options = { collapse_vars: true, diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index f92f40ca..7a5e66b5 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -299,7 +299,7 @@ unsafe_evaluate_modified: { console.log(function(){ var o={p:1}; o.p++; console.log(o.p); return o.p; }()); console.log(function(){ var o={p:2}; --o.p; console.log(o.p); return o.p; }()); console.log(function(){ var o={p:3}; o.p += ""; console.log(o.p); return o.p; }()); - console.log(function(){ var o={p:4}; o = {}; console.log(o.p); return o.p; }()); + console.log(function(){ var o; o = {}; console.log(o.p); return o.p; }()); console.log(function(){ var o={p:5}; o.p = -9; console.log(o.p); return o.p; }()); function inc() { this.p++; } console.log(function(){ var o={p:6}; inc.call(o); console.log(o.p); return o.p; }()); @@ -2312,8 +2312,7 @@ delay_def: { } expect: { function f() { - return a; - var a; + return; } function g() { return a; @@ -2324,6 +2323,28 @@ delay_def: { expect_stdout: true } +delay_def_lhs: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + console.log(function() { + long_name++; + return long_name; + var long_name; + }()); + } + expect: { + console.log(function() { + long_name++; + return long_name; + var long_name; + }()); + } + expect_stdout: "NaN" +} + booleans: { options = { booleans: true, @@ -5477,3 +5498,306 @@ issue_2670: { expect_stdout: "PASS" node_version: ">=6" } + +var_if: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + function f() { + if (x()) { + var a; + if (!g) a = true; + if (a) g(); + } + } + } + expect: { + function f() { + if (x()) { + var a; + if (!g) a = true; + if (a) g(); + } + } + } +} + +defun_assign: { + options = { + reduce_vars: true, + toplevel: true, + } + input: { + console.log(typeof a); + a = 42; + console.log(typeof a); + function a() {} + console.log(typeof a); + } + expect: { + console.log(typeof a); + a = 42; + console.log(typeof a); + function a() {} + console.log(typeof a); + } + expect_stdout: [ + "function", + "number", + "number", + ] +} + +defun_var_1: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + typeofs: true, + unused: true, + } + input: { + var a = 42, b; + function a() {} + function b() {} + console.log(typeof a, typeof b); + } + expect: { + var a = 42; + function a() {} + console.log(typeof a, "function"); + } + expect_stdout: "number function" +} + +defun_var_2: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + typeofs: true, + unused: true, + } + input: { + function a() {} + function b() {} + var a = 42, b; + console.log(typeof a, typeof b); + } + expect: { + function a() {} + var a = 42; + console.log(typeof a, "function"); + } + expect_stdout: "number function" +} + +defun_var_3: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + typeofs: true, + unused: true, + } + input: { + function a() {} + function b() {} + console.log(typeof a, typeof b); + var a = 42, b; + } + expect: { + function a() {} + console.log(typeof a, "function"); + var a = 42; + } + expect_stdout: "function function" +} + +defun_catch_1: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function a() {} + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect_stdout: "42" +} + +defun_catch_2: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + function a() {} + throw 42; + } catch (a) { + console.log(a); + } + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect_stdout: "42" +} + +defun_catch_3: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + throw 42; + function a() {} + } catch (a) { + console.log(a); + } + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect_stdout: "42" +} + +defun_catch_4: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + throw 42; + } catch (a) { + function a() {} + console.log(a); + } + } + expect: { + try { + throw 42; + } catch (a) { + function a() {} + console.log(a); + } + } + expect_stdout: true +} + +defun_catch_5: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + throw 42; + } catch (a) { + console.log(a); + function a() {} + } + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + function a() {} + } + } + expect_stdout: true +} + +defun_catch_6: { + options = { + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + try { + throw 42; + } catch (a) { + console.log(a); + } + function a() {} + } + expect: { + try { + throw 42; + } catch (a) { + console.log(a); + } + } + expect_stdout: "42" +} + +duplicate_lambda_defun_name_1: { + options = { + reduce_vars: true, + } + input: { + console.log(function f(a) { + function f() {} + return f.length; + }()); + } + expect: { + console.log(function f(a) { + function f() {} + return f.length; + }()); + } + expect_stdout: "0" +} + +duplicate_lambda_defun_name_2: { + options = { + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + console.log(function f(a) { + function f() {} + return f.length; + }()); + } + expect: { + console.log(function(a) { + return function() {}.length; + }()); + } + expect_stdout: "0" +} diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 835ab7ee..8ecb8b13 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -820,3 +820,66 @@ cascade_assignment_in_return: { } } } + +hoist_defun: { + options = { + join_vars: true, + sequences: true, + } + input: { + x(); + function f() {} + y(); + } + expect: { + function f() {} + x(), y(); + } +} + +hoist_decl: { + options = { + join_vars: true, + sequences: true, + } + input: { + var a; + w(); + var b = x(); + y(); + for (var c; 0;) z(); + var d; + } + expect: { + var a; + w(); + var b = x(), c, d; + for (y(); 0;) z(); + } +} + +for_init_var: { + options = { + join_vars: true, + unused: false, + } + input: { + var a = "PASS"; + (function() { + var b = 42; + for (var c = 5; c > 0;) c--; + a = "FAIL"; + var a; + })(); + console.log(a); + } + expect: { + var a = "PASS"; + (function() { + for (var b = 42, c = 5, a; c > 0;) c--; + a = "FAIL"; + })(); + console.log(a); + } + expect_stdout: "PASS" +} diff --git a/test/compress/typeof.js b/test/compress/typeof.js index 180e5451..b5f14016 100644 --- a/test/compress/typeof.js +++ b/test/compress/typeof.js @@ -100,7 +100,7 @@ typeof_defun_1: { g = 42; console.log("YES"); "function" == typeof g && g(); - h(); + "function" == typeof h && h(); } expect_stdout: [ "YES", @@ -138,3 +138,166 @@ typeof_defun_2: { "2", ] } + +duplicate_defun_arg_name: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + function long_name(long_name) { + return typeof long_name; + } + console.log(typeof long_name, long_name()); + } + expect: { + function long_name(long_name) { + return typeof long_name; + } + console.log(typeof long_name, long_name()); + } + expect_stdout: "function undefined" +} + +duplicate_lambda_arg_name: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + console.log(function long_name(long_name) { + return typeof long_name; + }()); + } + expect: { + console.log(function long_name(long_name) { + return typeof long_name; + }()); + } + expect_stdout: "undefined" +} + +issue_2728_1: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + (function arguments() { + console.log(typeof arguments); + })(); + } + expect: { + (function arguments() { + console.log(typeof arguments); + })(); + } + expect_stdout: "object" +} + +issue_2728_2: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + function arguments() { + return typeof arguments; + } + console.log(typeof arguments, arguments()); + } + expect: { + function arguments() { + return typeof arguments; + } + console.log(typeof arguments, arguments()); + } + expect_stdout: "function object" +} + +issue_2728_3: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + (function() { + function arguments() { + } + console.log(typeof arguments); + })(); + } + expect: { + (function() { + function arguments() { + } + console.log("function"); + })(); + } + expect_stdout: "function" +} + +issue_2728_4: { + options = { + evaluate: true, + reduce_vars: true, + toplevel: true, + typeofs: true, + } + input: { + function arguments() { + } + console.log(typeof arguments); + } + expect: { + function arguments() { + } + console.log("function"); + } + expect_stdout: "function" +} + +issue_2728_5: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + (function arguments(arguments) { + console.log(typeof arguments); + })(); + } + expect: { + (function arguments(arguments) { + console.log(typeof arguments); + })(); + } + expect_stdout: "undefined" +} + +issue_2728_6: { + options = { + evaluate: true, + reduce_vars: true, + typeofs: true, + } + input: { + function arguments(arguments) { + return typeof arguments; + } + console.log(typeof arguments, arguments()); + } + expect: { + function arguments(arguments) { + return typeof arguments; + } + console.log(typeof arguments, arguments()); + } + expect_stdout: "function undefined" +} diff --git a/test/input/issue-1242/qux.js b/test/input/issue-1242/qux.js index 94171f38..4a72ffc2 100644 --- a/test/input/issue-1242/qux.js +++ b/test/input/issue-1242/qux.js @@ -1,4 +1,4 @@ -var a = bar(1+2); -var b = baz(3+9); -print('q' + 'u' + 'x', a, b); +var x = bar(1+2); +var y = baz(3+9); +print('q' + 'u' + 'x', x, y); foo(5+6); diff --git a/test/jetstream.js b/test/jetstream.js index 1cc6d4a7..7ee09df3 100644 --- a/test/jetstream.js +++ b/test/jetstream.js @@ -5,11 +5,7 @@ var site = "http://browserbench.org/JetStream"; if (typeof phantom == "undefined") { - // 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); - }); + require("../tools/exit"); var args = process.argv.slice(2); var debug = args.indexOf("--debug"); if (debug >= 0) { diff --git a/test/mocha/comment.js b/test/mocha/comment.js index 5968b569..c0b13b53 100644 --- a/test/mocha/comment.js +++ b/test/mocha/comment.js @@ -220,4 +220,24 @@ describe("Comment", function() { if (result.error) throw result.error; assert.strictEqual(result.code, "/*a*/ /*b*/(function(){/*c*/}/*d*/ /*e*/)();"); }); + + it("Should output line comments after statements", function() { + var result = uglify.minify([ + "x()//foo", + "{y()//bar", + "}", + ].join("\n"), { + compress: false, + mangle: false, + output: { + comments: "all", + }, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code, [ + "x();//foo", + "{y();//bar", + "}", + ].join("\n")); + }); }); diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 6bae59c9..e68ab2a2 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -42,8 +42,15 @@ describe("minify", function() { original += code; compressed += result.code; }); - assert.strictEqual(JSON.stringify(cache).slice(0, 20), '{"cname":5,"props":{'); - assert.strictEqual(compressed, 'function n(n){return 3*n}function r(n){return n/2}var c=console.log.bind(console);function l(o){c("Foo:",2*o)}var f=n(3),i=r(12);c("qux",f,i),l(11);'); + assert.strictEqual(JSON.stringify(cache).slice(0, 10), '{"props":{'); + assert.strictEqual(compressed, [ + "function n(n){return 3*n}", + "function r(n){return n/2}", + "var o=console.log.bind(console);", + 'function c(n){o("Foo:",2*n)}', + "var a=n(3),b=r(12);", + 'o("qux",a,b),c(11);', + ].join("")); assert.strictEqual(run_code(compressed), run_code(original)); }); @@ -68,12 +75,48 @@ describe("minify", function() { original += code; compressed += result.code; }); - assert.strictEqual(JSON.stringify(cache).slice(0, 28), '{"vars":{"cname":5,"props":{'); - assert.strictEqual(compressed, 'function n(n){return 3*n}function r(n){return n/2}var c=console.log.bind(console);function l(o){c("Foo:",2*o)}var f=n(3),i=r(12);c("qux",f,i),l(11);'); + assert.strictEqual(JSON.stringify(cache).slice(0, 18), '{"vars":{"props":{'); + assert.strictEqual(compressed, [ + "function n(n){return 3*n}", + "function r(n){return n/2}", + "var o=console.log.bind(console);", + 'function c(n){o("Foo:",2*n)}', + "var a=n(3),b=r(12);", + 'o("qux",a,b),c(11);', + ].join("")); assert.strictEqual(run_code(compressed), run_code(original)); }); - it("should not parse invalid use of reserved words", function() { + it("Should avoid mangled names in cache", function() { + var cache = {}; + var original = ""; + var compressed = ""; + [ + '"xxxyy";var i={s:1};', + '"xxyyy";var j={t:2,u:3},k=4;', + 'console.log(i.s,j.t,j.u,k);', + ].forEach(function(code) { + var result = Uglify.minify(code, { + compress: false, + mangle: { + properties: true, + toplevel: true + }, + nameCache: cache + }); + if (result.error) throw result.error; + original += code; + compressed += result.code; + }); + assert.strictEqual(compressed, [ + '"xxxyy";var x={x:1};', + '"xxyyy";var y={y:2,a:3},a=4;', + 'console.log(x.x,y.y,y.a,a);', + ].join("")); + assert.strictEqual(run_code(compressed), run_code(original)); + }); + + it("Should not parse invalid use of reserved words", function() { assert.strictEqual(Uglify.minify("function enum(){}").error, undefined); assert.strictEqual(Uglify.minify("function static(){}").error, undefined); assert.strictEqual(Uglify.minify("function super(){}").error.message, "Unexpected token: name (super)"); diff --git a/test/run-tests.js b/test/run-tests.js index e95bbb83..5dcacd87 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -10,6 +10,7 @@ var semver = require("semver"); var tests_dir = path.dirname(module.filename); var failures = 0; var failed_files = {}; +var minify_options = require("./ufuzz.json").map(JSON.stringify); run_compress_tests(); if (failures) { @@ -100,6 +101,15 @@ function run_compress_tests() { quote_style: 3, keep_quoted_props: true }); + try { + U.parse(input_code); + } catch (ex) { + log("!!! Cannot parse input\n---INPUT---\n{input}\n--PARSE ERROR--\n{error}\n\n", { + input: input_formatted, + error: ex, + }); + return false; + } var options = U.defaults(test.options, { warnings: false }); @@ -139,78 +149,77 @@ function run_compress_tests() { output: output, expected: expect }); - failures++; - failed_files[file] = 1; + return false; } - else { - // expect == output - try { - 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", { + // expect == output + try { + 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_formatted, + output: output, + error: ex, + }); + return false; + } + if (test.expect_warnings) { + U.AST_Node.warn_function = original_warn_function; + var expected_warnings = make_code(test.expect_warnings, { + beautify: false, + quote_style: 2, // force double quote to match JSON + }); + warnings_emitted = warnings_emitted.map(function(input) { + return input.split(process.cwd() + path.sep).join("").split(path.sep).join("/"); + }); + 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_formatted, - output: output, - error: ex.toString(), + expected_warnings: expected_warnings, + actual_warnings: actual_warnings, }); - failures++; - failed_files[file] = 1; - } - if (test.expect_warnings) { - U.AST_Node.warn_function = original_warn_function; - var expected_warnings = make_code(test.expect_warnings, { - beautify: false, - quote_style: 2, // force double quote to match JSON - }); - warnings_emitted = warnings_emitted.map(function(input) { - return input.split(process.cwd() + path.sep).join("").split(path.sep).join("/"); - }); - 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_formatted, - expected_warnings: expected_warnings, - actual_warnings: actual_warnings, - }); - failures++; - failed_files[file] = 1; - } - } - if (test.expect_stdout - && (!test.node_version || semver.satisfies(process.version, test.node_version))) { - var stdout = sandbox.run_code(input_code); - if (test.expect_stdout === true) { - test.expect_stdout = stdout; - } - if (!sandbox.same_stdout(test.expect_stdout, stdout)) { - log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { - input: input_formatted, - expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", - expected: test.expect_stdout, - actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", - actual: stdout, - }); - failures++; - failed_files[file] = 1; - } else { - stdout = sandbox.run_code(output); - if (!sandbox.same_stdout(test.expect_stdout, stdout)) { - log("!!! failed\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { - input: input_formatted, - expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", - expected: test.expect_stdout, - actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", - actual: stdout, - }); - failures++; - failed_files[file] = 1; - } - } + return false; } } + if (test.expect_stdout + && (!test.node_version || semver.satisfies(process.version, test.node_version))) { + var stdout = sandbox.run_code(input_code); + if (test.expect_stdout === true) { + test.expect_stdout = stdout; + } + if (!sandbox.same_stdout(test.expect_stdout, stdout)) { + log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { + input: input_formatted, + expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", + expected: test.expect_stdout, + actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", + actual: stdout, + }); + return false; + } + stdout = sandbox.run_code(output); + if (!sandbox.same_stdout(test.expect_stdout, stdout)) { + log("!!! failed\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { + input: input_formatted, + expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", + expected: test.expect_stdout, + actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", + actual: stdout, + }); + return false; + } + if (!reminify(test.options, input_code, input_formatted, test.expect_stdout)) { + return false; + } + } + return true; } var tests = parse_test(path.resolve(dir, file)); for (var i in tests) if (tests.hasOwnProperty(i)) { - test_case(tests[i]); + if (!test_case(tests[i])) { + failures++; + failed_files[file] = 1; + } } } files.forEach(function(file){ @@ -346,3 +355,46 @@ function evaluate(code) { code = make_code(code, { beautify: true }); return new Function("return(" + code + ")")(); } + +// Try to reminify original input with standard options +// to see if it matches expect_stdout. +function reminify(orig_options, input_code, input_formatted, expect_stdout) { + for (var i = 0; i < minify_options.length; i++) { + var options = JSON.parse(minify_options[i]); + if (options.compress) [ + "keep_fargs", + "keep_fnames", + ].forEach(function(name) { + if (name in orig_options) { + options.compress[name] = orig_options[name]; + } + }); + var options_formatted = JSON.stringify(options, null, 4); + var result = U.minify(input_code, options); + if (result.error) { + log("!!! failed input reminify\n---INPUT---\n{input}\n--ERROR---\n{error}\n\n", { + input: input_formatted, + error: result.error, + }); + return false; + } else { + var stdout = sandbox.run_code(result.code); + if (typeof expect_stdout != "string" && typeof stdout != "string" && expect_stdout.name == stdout.name) { + stdout = expect_stdout; + } + if (!sandbox.same_stdout(expect_stdout, stdout)) { + log("!!! failed running reminified input\n---INPUT---\n{input}\n---OPTIONS---\n{options}\n---OUTPUT---\n{output}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { + input: input_formatted, + options: options_formatted, + output: result.code, + expected_type: typeof expect_stdout == "string" ? "STDOUT" : "ERROR", + expected: expect_stdout, + actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", + actual: stdout, + }); + return false; + } + } + } + return true; +} diff --git a/test/ufuzz.js b/test/ufuzz.js index 1589d5f1..d02e9f76 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -6,11 +6,7 @@ // bin/uglifyjs s.js -c && bin/uglifyjs s.js -c passes=3 && bin/uglifyjs s.js -c passes=3 -m // cat s.js | node && node s.js && bin/uglifyjs s.js -c | node && bin/uglifyjs s.js -c passes=3 | node && bin/uglifyjs s.js -c passes=3 -m | node -// workaround for tty output truncation upon process.exit() -[process.stdout, process.stderr].forEach(function(stream){ - if (stream._handle && stream._handle.setBlocking) - stream._handle.setBlocking(true); -}); +require("../tools/exit"); var UglifyJS = require(".."); var randomBytes = require("crypto").randomBytes; @@ -127,6 +123,9 @@ for (var i = 2; i < process.argv.length; ++i) { } var VALUES = [ + '"a"', + '"b"', + '"c"', '""', 'true', 'false', diff --git a/tools/exit.js b/tools/exit.js new file mode 100644 index 00000000..17048d8e --- /dev/null +++ b/tools/exit.js @@ -0,0 +1,15 @@ +// workaround for tty output truncation upon process.exit() +var exit = process.exit; +process.exit = function() { + var args = [].slice.call(arguments); + process.once("uncaughtException", function() { + (function callback() { + if (process.stdout.bufferSize || process.stderr.bufferSize) { + setImmediate(callback); + } else { + exit.apply(process, args); + } + })(); + }); + throw exit; +};