From 381bd3836ecd79eb5d4f7b84807c778ee1acf9c9 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 14 Mar 2017 13:19:05 +0800 Subject: [PATCH 01/11] minor clean-ups (#1600) - remove obsolete optimisation in `AST_Binary` after #1477 - improve `TreeWalker.has_directive()` readability and resilience against multiple visits --- lib/ast.js | 6 +++--- lib/compress.js | 12 +----------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index f7ab52e2..092a9590 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -984,8 +984,8 @@ TreeWalker.prototype = { push: function (node) { if (node instanceof AST_Lambda) { this.directives = Object.create(this.directives); - } else if (node instanceof AST_Directive) { - this.directives[node.value] = this.directives[node.value] ? "up" : true; + } else if (node instanceof AST_Directive && !this.directives[node.value]) { + this.directives[node.value] = node; } this.stack.push(node); }, @@ -1013,7 +1013,7 @@ TreeWalker.prototype = { for (var i = 0; i < node.body.length; ++i) { var st = node.body[i]; if (!(st instanceof AST_Directive)) break; - if (st.value == type) return true; + if (st.value == type) return st; } } }, diff --git a/lib/compress.js b/lib/compress.js index e3ae5bde..ab4c3c2f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1664,7 +1664,7 @@ merge(Compressor.prototype, { /* -----[ optimizers ]----- */ OPT(AST_Directive, function(self, compressor){ - if (compressor.has_directive(self.value) === "up") { + if (compressor.has_directive(self.value) !== self) { return make_node(AST_EmptyStatement, self); } return self; @@ -3018,16 +3018,6 @@ merge(Compressor.prototype, { var commutativeOperators = makePredicate("== === != !== * & | ^"); OPT(AST_Binary, function(self, compressor){ - var lhs = self.left.evaluate(compressor); - var rhs = self.right.evaluate(compressor); - if (lhs.length > 1 && lhs[0].is_constant() !== self.left.is_constant() - || rhs.length > 1 && rhs[0].is_constant() !== self.right.is_constant()) { - return make_node(AST_Binary, self, { - operator: self.operator, - left: lhs[0], - right: rhs[0] - }).optimize(compressor); - } function reversible() { return self.left instanceof AST_Constant || self.right instanceof AST_Constant From 8223b2e0db4cc41d467d9b94b05511a36c320184 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 15 Mar 2017 18:44:13 +0800 Subject: [PATCH 02/11] fix `AST_Node.optimize()` (#1602) Liberal use of `Compressor.transform()` and `AST_Node.optimize()` presents an issue for look-up operations like `TreeWalker.in_boolean_context()` and `TreeWalker.parent()`. This is an incremental fix such that `AST_Node.optimize()` would now contain the correct stack information when called correctly. --- lib/compress.js | 69 +++++++++----------- lib/utils.js | 4 +- test/compress/transform.js | 129 +++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 38 deletions(-) create mode 100644 test/compress/transform.js diff --git a/lib/compress.js b/lib/compress.js index ab4c3c2f..59a96684 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -152,13 +152,14 @@ merge(Compressor.prototype, { was_scope = true; } descend(node, this); - node = node.optimize(this); - if (was_scope && node instanceof AST_Scope) { - node.drop_unused(this); - descend(node, this); + descend(node, this); + var opt = node.optimize(this); + if (was_scope && opt instanceof AST_Scope) { + opt.drop_unused(this); + descend(opt, this); } - node._squeezed = true; - return node; + if (opt === node) opt._squeezed = true; + return opt; } }); @@ -171,8 +172,7 @@ merge(Compressor.prototype, { if (compressor.has_directive("use asm")) return self; var opt = optimizer(self, compressor); opt._optimized = true; - if (opt === self) return opt; - return opt.transform(compressor); + return opt; }); }; @@ -914,7 +914,7 @@ merge(Compressor.prototype, { if (stat instanceof AST_LoopControl) { var lct = compressor.loopcontrol_target(stat.label); if ((stat instanceof AST_Break - && lct instanceof AST_BlockStatement + && !(lct instanceof AST_IterationStatement) && loop_body(lct) === self) || (stat instanceof AST_Continue && loop_body(lct) === self)) { if (stat.label) { @@ -1646,8 +1646,8 @@ merge(Compressor.prototype, { return thing && thing.aborts(); }; (function(def){ - def(AST_Statement, function(){ return null }); - def(AST_Jump, function(){ return this }); + def(AST_Statement, return_null); + def(AST_Jump, return_this); function block_aborts(){ var n = this.body.length; return n > 0 && aborts(this.body[n - 1]); @@ -2077,14 +2077,6 @@ merge(Compressor.prototype, { // drop_side_effect_free() // remove side-effect-free parts which only affects return value (function(def){ - function return_this() { - return this; - } - - function return_null() { - return null; - } - // Drop side-effect-free elements from an array of expressions. // Returns an array of expressions with side-effects or null // if all elements were dropped. Note: original array may be @@ -2358,7 +2350,7 @@ merge(Compressor.prototype, { extract_declarations_from_unreachable_code(compressor, self.alternative, a); } a.push(self.body); - return make_node(AST_BlockStatement, self, { body: a }).transform(compressor); + return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } } else { compressor.warn("Condition always false [{file}:{line},{col}]", self.condition.start); @@ -2366,7 +2358,7 @@ merge(Compressor.prototype, { var a = []; extract_declarations_from_unreachable_code(compressor, self.body, a); if (self.alternative) a.push(self.alternative); - return make_node(AST_BlockStatement, self, { body: a }).transform(compressor); + return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } } } @@ -2385,8 +2377,8 @@ merge(Compressor.prototype, { } if (is_empty(self.body) && is_empty(self.alternative)) { return make_node(AST_SimpleStatement, self.condition, { - body: self.condition - }).transform(compressor); + body: self.condition.clone() + }).optimize(compressor); } if (self.body instanceof AST_SimpleStatement && self.alternative instanceof AST_SimpleStatement) { @@ -2396,7 +2388,7 @@ merge(Compressor.prototype, { consequent : statement_to_expression(self.body), alternative : statement_to_expression(self.alternative) }) - }).transform(compressor); + }).optimize(compressor); } if (is_empty(self.alternative) && self.body instanceof AST_SimpleStatement) { if (self_condition_length === negated_length && !negated_is_best @@ -2412,14 +2404,14 @@ merge(Compressor.prototype, { left : negated, right : statement_to_expression(self.body) }) - }).transform(compressor); + }).optimize(compressor); return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "&&", left : self.condition, right : statement_to_expression(self.body) }) - }).transform(compressor); + }).optimize(compressor); } if (self.body instanceof AST_EmptyStatement && self.alternative @@ -2430,7 +2422,7 @@ merge(Compressor.prototype, { left : self.condition, right : statement_to_expression(self.alternative) }) - }).transform(compressor); + }).optimize(compressor); } if (self.body instanceof AST_Exit && self.alternative instanceof AST_Exit @@ -2440,18 +2432,21 @@ merge(Compressor.prototype, { condition : self.condition, consequent : self.body.value || make_node(AST_Undefined, self.body), alternative : self.alternative.value || make_node(AST_Undefined, self.alternative) - }) - }).transform(compressor); + }).transform(compressor) + }).optimize(compressor); } if (self.body instanceof AST_If && !self.body.alternative && !self.alternative) { - self.condition = make_node(AST_Binary, self.condition, { - operator: "&&", - left: self.condition, - right: self.body.condition - }).transform(compressor); - self.body = self.body.body; + self = make_node(AST_If, self, { + condition: make_node(AST_Binary, self.condition, { + operator: "&&", + left: self.condition, + right: self.body.condition + }), + body: self.body.body, + alternative: null + }); } if (aborts(self.body)) { if (self.alternative) { @@ -2459,7 +2454,7 @@ merge(Compressor.prototype, { self.alternative = null; return make_node(AST_BlockStatement, self, { body: [ self, alt ] - }).transform(compressor); + }).optimize(compressor); } } if (aborts(self.alternative)) { @@ -2469,7 +2464,7 @@ merge(Compressor.prototype, { self.alternative = null; return make_node(AST_BlockStatement, self, { body: [ self, body ] - }).transform(compressor); + }).optimize(compressor); } return self; }); diff --git a/lib/utils.js b/lib/utils.js index da663546..fdb20471 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -126,9 +126,11 @@ function merge(obj, ext) { return count; }; -function noop() {}; +function noop() {} function return_false() { return false; } function return_true() { return true; } +function return_this() { return this; } +function return_null() { return null; } var MAP = (function(){ function MAP(a, f, backwards) { diff --git a/test/compress/transform.js b/test/compress/transform.js new file mode 100644 index 00000000..7b616e40 --- /dev/null +++ b/test/compress/transform.js @@ -0,0 +1,129 @@ +booleans_evaluate: { + options = { + booleans: true, + evaluate: true, + } + input: { + console.log(typeof void 0 != "undefined"); + console.log(1 == 1, 1 === 1) + console.log(1 != 1, 1 !== 1) + } + expect: { + console.log(!1); + console.log(!0, !0); + console.log(!1, !1); + } +} + +booleans_global_defs: { + options = { + booleans: true, + evaluate: true, + global_defs: { + A: true, + }, + } + input: { + console.log(A == 1); + } + expect: { + console.log(!0); + } +} + +condition_evaluate: { + options = { + booleans: true, + dead_code: false, + evaluate: true, + loops: false, + } + input: { + while (1 === 2); + for (; 1 == true;); + if (void 0 == null); + } + expect: { + while (!1); + for (; !0;); + if (!0); + } +} + +if_else_empty: { + options = { + conditionals: true, + } + input: { + if ({} ? a : b); else {} + } + expect: { + !{} ? b : a; + } +} + +label_if_break: { + options = { + conditionals: true, + dead_code: true, + evaluate: true, + } + input: { + L: if (true) { + a; + break L; + } + } + expect: { + a; + } +} + +while_if_break: { + options = { + conditionals: true, + loops: true, + sequences: true, + } + input: { + while (a) { + if (b) if(c) d; + if (e) break; + } + } + expect: { + for(; a && (b && c && d, !e);); + } +} + +if_return: { + options = { + booleans: true, + conditionals: true, + if_return: true, + sequences: true, + } + input: { + function f(w, x, y, z) { + if (x) return; + if (w) { + if (y) return; + } else if (z) return; + if (x == y) return true; + + if (x) w(); + if (y) z(); + return true; + } + } + expect: { + function f(w, x, y, z) { + if (!x) { + if (w) { + if (y) return; + } else if (z) return; + return x == y || (x && w(), y && z(), !0); + } + } + } +} From cf4bf4ceb1eee86197d51e77e640e59ca04739b8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 16 Mar 2017 01:02:59 +0800 Subject: [PATCH 03/11] fix stack issues with `AST_Node.evaluate()` (#1603) As patched in #1597, `make_node_from_constant()` makes inconsistent and sometimes incorrect calls to `optimize()` and `transform()`. Fix those issues properly by changing the semantics of `evaluate()` and `make_node_from_constant()`, with the side effect that `evaluate()` no longer eagerly converts constant to `AST_Node`. --- lib/compress.js | 261 ++++++++++++++++++++++++++---------------------- 1 file changed, 141 insertions(+), 120 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 59a96684..b3004fb5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -98,10 +98,10 @@ function Compressor(options, false_by_default) { this.top_retain = function(def) { return top_retain.test(def.name); }; - } else if (typeof top_retain === "function") { + } else if (typeof top_retain == "function") { this.top_retain = top_retain; } else if (top_retain) { - if (typeof top_retain === "string") { + if (typeof top_retain == "string") { top_retain = top_retain.split(/,/); } this.top_retain = function(def) { @@ -151,7 +151,17 @@ merge(Compressor.prototype, { node = node.hoist_declarations(this); was_scope = true; } + // Before https://github.com/mishoo/UglifyJS2/pull/1602 AST_Node.optimize() + // would call AST_Node.transform() if a different instance of AST_Node is + // produced after OPT(). + // This corrupts TreeWalker.stack, which cause AST look-ups to malfunction. + // Migrate and defer all children's AST_Node.transform() to below, which + // will now happen after this parent AST_Node has been properly substituted + // thus gives a consistent AST snapshot. descend(node, this); + // Existing code relies on how AST_Node.optimize() worked, and omitting the + // following replacement call would result in degraded efficiency of both + // output and performance. descend(node, this); var opt = node.optimize(this); if (was_scope && opt instanceof AST_Scope) { @@ -384,7 +394,7 @@ merge(Compressor.prototype, { return new ctor(props); }; - function make_node_from_constant(compressor, val, orig) { + function make_node_from_constant(val, orig) { switch (typeof val) { case "string": return make_node(AST_String, orig, { @@ -404,9 +414,9 @@ merge(Compressor.prototype, { return make_node(AST_Number, orig, { value: val }); case "boolean": - return make_node(val ? AST_True : AST_False, orig).optimize(compressor); + return make_node(val ? AST_True : AST_False, orig); case "undefined": - return make_node(AST_Undefined, orig).transform(compressor); + return make_node(AST_Undefined, orig); default: if (val === null) { return make_node(AST_Null, orig, { value: null }); @@ -1198,11 +1208,11 @@ merge(Compressor.prototype, { } } }); - function to_node(compressor, value, orig) { + function to_node(value, orig) { if (value instanceof AST_Node) return make_node(value.CTOR, orig, value); if (Array.isArray(value)) return make_node(AST_Array, orig, { elements: value.map(function(value) { - return to_node(compressor, value, orig); + return to_node(value, orig); }) }); if (value && typeof value == "object") { @@ -1210,14 +1220,14 @@ merge(Compressor.prototype, { for (var key in value) { props.push(make_node(AST_ObjectKeyVal, orig, { key: key, - value: to_node(compressor, value[key], orig) + value: to_node(value[key], orig) })); } return make_node(AST_Object, orig, { properties: props }); } - return make_node_from_constant(compressor, value, orig); + return make_node_from_constant(value, orig); } def(AST_Node, noop); def(AST_Dot, function(compressor, suffix){ @@ -1228,7 +1238,7 @@ merge(Compressor.prototype, { var name; var defines = compressor.option("global_defs"); if (defines && HOP(defines, (name = this.name + suffix))) { - var node = to_node(compressor, defines[name], this); + var node = to_node(defines[name], this); var top = compressor.find_parent(AST_Toplevel); node.walk(new TreeWalker(function(node) { if (node instanceof AST_SymbolRef) { @@ -1243,45 +1253,40 @@ merge(Compressor.prototype, { node.DEFMETHOD("_find_defs", func); }); - function best_of(ast1, ast2) { + function best_of_expression(ast1, ast2) { return ast1.print_to_string().length > ast2.print_to_string().length ? ast2 : ast1; } function best_of_statement(ast1, ast2) { - return best_of(make_node(AST_SimpleStatement, ast1, { + return best_of_expression(make_node(AST_SimpleStatement, ast1, { body: ast1 }), make_node(AST_SimpleStatement, ast2, { body: ast2 })).body; } + function best_of(compressor, ast1, ast2) { + return (first_in_statement(compressor) ? best_of_statement : best_of_expression)(ast1, ast2); + } + // methods to evaluate a constant expression (function (def){ - // The evaluate method returns an array with one or two - // elements. If the node has been successfully reduced to a - // constant, then the second element tells us the value; - // otherwise the second element is missing. The first element - // of the array is always an AST_Node descendant; if - // evaluation was successful it's a node that represents the - // constant; otherwise it's the original or a replacement node. + // If the node has been successfully reduced to a constant, + // then its value is returned; otherwise the element itself + // is returned. + // They can be distinguished as constant value is never a + // descendant of AST_Node. AST_Node.DEFMETHOD("evaluate", function(compressor){ - if (!compressor.option("evaluate")) return [ this ]; - var val; + if (!compressor.option("evaluate")) return this; try { - val = this._eval(compressor); + var val = this._eval(compressor); + return !val || val instanceof RegExp || typeof val != "object" ? val : this; } catch(ex) { if (ex !== def) throw ex; - return [ this ]; + return this; } - var node; - try { - node = make_node_from_constant(compressor, val, this); - } catch(ex) { - return [ this ]; - } - return [ best_of(node, this), val ]; }); var unaryPrefix = makePredicate("! ~ - +"); AST_Node.DEFMETHOD("is_constant", function(){ @@ -1319,8 +1324,8 @@ merge(Compressor.prototype, { })); } var result = this.evaluate(compressor); - if (result.length > 1) { - return result[1]; + if (result !== this) { + return result; } throw new Error(string_template("Cannot evaluate constant [{file}:{line},{col}]", this.start)); }); @@ -1480,9 +1485,9 @@ merge(Compressor.prototype, { var stat = make_node(AST_SimpleStatement, alt, { body: alt }); - return best_of(negated, stat) === stat ? alt : negated; + return best_of_expression(negated, stat) === stat ? alt : negated; } - return best_of(negated, alt); + return best_of_expression(negated, alt); } def(AST_Node, function(){ return basic_negation(this); @@ -2233,27 +2238,24 @@ merge(Compressor.prototype, { }); OPT(AST_DWLoop, function(self, compressor){ - var cond = self.condition.evaluate(compressor); - self.condition = cond[0]; if (!compressor.option("loops")) return self; - if (cond.length > 1) { - if (cond[1]) { + var cond = self.condition.evaluate(compressor); + if (cond !== self.condition) { + if (cond) { return make_node(AST_For, self, { body: self.body }); - } else if (self instanceof AST_While) { - if (compressor.option("dead_code")) { - var a = []; - extract_declarations_from_unreachable_code(compressor, self.body, a); - return make_node(AST_BlockStatement, self, { body: a }); - } + } else if (compressor.option("dead_code") && self instanceof AST_While) { + var a = []; + extract_declarations_from_unreachable_code(compressor, self.body, a); + return make_node(AST_BlockStatement, self, { body: a }); } else { - // self instanceof AST_Do - return self; + cond = make_node_from_constant(cond, self.condition).transform(compressor); + self.condition = best_of_expression(cond, self.condition); } } if (self instanceof AST_While) { - return make_node(AST_For, self, self).transform(compressor); + return make_node(AST_For, self, self).optimize(compressor); } return self; }); @@ -2275,7 +2277,7 @@ merge(Compressor.prototype, { var first = self.body instanceof AST_BlockStatement ? self.body.body[0] : self.body; if (first instanceof AST_If) { if (first.body instanceof AST_Break - && compressor.loopcontrol_target(first.body.label) === self) { + && compressor.loopcontrol_target(first.body.label) === compressor.self()) { if (self.condition) { self.condition = make_node(AST_Binary, self.condition, { left: self.condition, @@ -2288,7 +2290,7 @@ merge(Compressor.prototype, { drop_it(first.alternative); } else if (first.alternative instanceof AST_Break - && compressor.loopcontrol_target(first.alternative.label) === self) { + && compressor.loopcontrol_target(first.alternative.label) === compressor.self()) { if (self.condition) { self.condition = make_node(AST_Binary, self.condition, { left: self.condition, @@ -2304,27 +2306,25 @@ merge(Compressor.prototype, { }; OPT(AST_For, function(self, compressor){ - var cond = self.condition; - if (cond) { - cond = cond.evaluate(compressor); - self.condition = cond[0]; - } if (!compressor.option("loops")) return self; - if (cond) { - if (cond.length > 1 && !cond[1]) { - if (compressor.option("dead_code")) { - var a = []; - if (self.init instanceof AST_Statement) { - a.push(self.init); - } - else if (self.init) { - a.push(make_node(AST_SimpleStatement, self.init, { - body: self.init - })); - } - extract_declarations_from_unreachable_code(compressor, self.body, a); - return make_node(AST_BlockStatement, self, { body: a }); + if (self.condition) { + var cond = self.condition.evaluate(compressor); + if (compressor.option("dead_code") && !cond) { + var a = []; + if (self.init instanceof AST_Statement) { + a.push(self.init); } + else if (self.init) { + a.push(make_node(AST_SimpleStatement, self.init, { + body: self.init + })); + } + extract_declarations_from_unreachable_code(compressor, self.body, a); + return make_node(AST_BlockStatement, self, { body: a }); + } + if (cond !== self.condition) { + cond = make_node_from_constant(cond, self.condition).transform(compressor); + self.condition = best_of_expression(cond, self.condition); } } if_break_in_loop(self, compressor); @@ -2340,9 +2340,8 @@ merge(Compressor.prototype, { // “has no side effects”; also it doesn't work for cases like // `x && true`, though it probably should. var cond = self.condition.evaluate(compressor); - self.condition = cond[0]; - if (cond.length > 1) { - if (cond[1]) { + if (cond !== self.condition) { + if (cond) { compressor.warn("Condition always true [{file}:{line},{col}]", self.condition.start); if (compressor.option("dead_code")) { var a = []; @@ -2361,6 +2360,8 @@ merge(Compressor.prototype, { return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } } + cond = make_node_from_constant(cond, self.condition).transform(compressor); + self.condition = best_of_expression(cond, self.condition); } var negated = self.condition.negate(compressor); var self_condition_length = self.condition.print_to_string().length; @@ -2488,12 +2489,12 @@ merge(Compressor.prototype, { } break; } - var exp = self.expression.evaluate(compressor); - out: if (exp.length == 2) try { + var value = self.expression.evaluate(compressor); + out: if (value !== self.expression) try { // constant expression - self.expression = exp[0]; + 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 value = exp[1]; var in_if = false; var in_block = false; var started = false; @@ -2540,11 +2541,11 @@ merge(Compressor.prototype, { if (stopped) return MAP.skip; if (node instanceof AST_Case) { var exp = node.expression.evaluate(compressor); - if (exp.length < 2) { + if (exp === node.expression) { // got a case with non-constant expression, baling out throw self; } - if (exp[1] === value || started) { + if (exp === value || started) { started = true; if (aborts(node)) stopped = true; descend(node, this); @@ -2758,15 +2759,14 @@ merge(Compressor.prototype, { var separator; if (self.args.length > 0) { separator = self.args[0].evaluate(compressor); - if (separator.length < 2) break EXIT; // not a constant - separator = separator[1]; + if (separator === self.args[0]) break EXIT; // not a constant } var elements = []; var consts = []; exp.expression.elements.forEach(function(el) { - el = el.evaluate(compressor); - if (el.length > 1) { - consts.push(el[1]); + var value = el.evaluate(compressor); + if (value !== el) { + consts.push(value); } else { if (consts.length > 0) { elements.push(make_node(AST_String, self, { @@ -2774,7 +2774,7 @@ merge(Compressor.prototype, { })); consts.length = 0; } - elements.push(el[0]); + elements.push(el); } }); if (consts.length > 0) { @@ -2815,7 +2815,7 @@ merge(Compressor.prototype, { node.expression = node.expression.clone(); node.expression.expression = node.expression.expression.clone(); node.expression.expression.elements = elements; - return best_of(self, node); + return best_of(compressor, self, node); } } if (exp instanceof AST_Function) { @@ -2961,8 +2961,7 @@ merge(Compressor.prototype, { return e.expression; } if (e instanceof AST_Binary) { - var statement = first_in_statement(compressor); - self = (statement ? best_of_statement : best_of)(self, e.negate(compressor, statement)); + self = best_of(compressor, self, e.negate(compressor, first_in_statement(compressor))); } break; case "typeof": @@ -2975,7 +2974,15 @@ merge(Compressor.prototype, { }).optimize(compressor); } } - return self.evaluate(compressor)[0]; + // avoids infinite recursion of numerals + if (self.operator != "-" || !(self.expression instanceof AST_Number)) { + var ev = self.evaluate(compressor); + if (ev !== self) { + ev = make_node_from_constant(ev, self).optimize(compressor); + return best_of(compressor, ev, self); + } + } + return self; }); function has_side_effects_or_prop_access(node, compressor) { @@ -3097,48 +3104,48 @@ merge(Compressor.prototype, { case "&&": var ll = self.left.evaluate(compressor); var rr = self.right.evaluate(compressor); - if ((ll.length > 1 && !ll[1]) || (rr.length > 1 && !rr[1])) { + if (!ll || !rr) { compressor.warn("Boolean && always false [{file}:{line},{col}]", self.start); return make_node(AST_Seq, self, { car: self.left, cdr: make_node(AST_False, self) }).optimize(compressor); } - if (ll.length > 1 && ll[1]) { - return rr[0]; + if (ll !== self.left && ll) { + return self.right.optimize(compressor); } - if (rr.length > 1 && rr[1]) { - return ll[0]; + if (rr !== self.right && rr) { + return self.left.optimize(compressor); } break; case "||": var ll = self.left.evaluate(compressor); var rr = self.right.evaluate(compressor); - if ((ll.length > 1 && ll[1]) || (rr.length > 1 && rr[1])) { + if (ll !== self.left && ll || rr !== self.right && rr) { compressor.warn("Boolean || always true [{file}:{line},{col}]", self.start); return make_node(AST_Seq, self, { car: self.left, cdr: make_node(AST_True, self) }).optimize(compressor); } - if (ll.length > 1 && !ll[1]) { - return rr[0]; + if (!ll) { + return self.right.optimize(compressor); } - if (rr.length > 1 && !rr[1]) { - return ll[0]; + if (!rr) { + return self.left.optimize(compressor); } break; case "+": var ll = self.left.evaluate(compressor); var rr = self.right.evaluate(compressor); - if (ll.length > 1 && ll[0] instanceof AST_String && ll[1]) { + if (ll && typeof ll == "string") { compressor.warn("+ in boolean context always true [{file}:{line},{col}]", self.start); return make_node(AST_Seq, self, { car: self.right, cdr: make_node(AST_True, self) }).optimize(compressor); } - if (rr.length > 1 && rr[0] instanceof AST_String && rr[1]) { + if (rr && typeof rr == "string") { compressor.warn("+ in boolean context always true [{file}:{line},{col}]", self.start); return make_node(AST_Seq, self, { car: self.left, @@ -3150,12 +3157,11 @@ merge(Compressor.prototype, { if (compressor.option("comparisons") && self.is_boolean()) { if (!(compressor.parent() instanceof AST_Binary) || compressor.parent() instanceof AST_Assign) { - var statement = first_in_statement(compressor); var negated = make_node(AST_UnaryPrefix, self, { operator: "!", - expression: self.negate(compressor, statement) + expression: self.negate(compressor, first_in_statement(compressor)) }); - self = (statement ? best_of_statement : best_of)(self, negated); + self = best_of(compressor, self, negated); } if (compressor.option("unsafe_comps")) { switch (self.operator) { @@ -3307,9 +3313,9 @@ merge(Compressor.prototype, { }); if (self.right instanceof AST_Constant && !(self.left instanceof AST_Constant)) { - self = best_of(reversed, self); + self = best_of(compressor, reversed, self); } else { - self = best_of(self, reversed); + self = best_of(compressor, self, reversed); } } if (associative && self.is_number(compressor)) { @@ -3406,7 +3412,12 @@ merge(Compressor.prototype, { self.right = self.right.right; return self.transform(compressor); } - return self.evaluate(compressor)[0]; + var ev = self.evaluate(compressor); + if (ev !== self) { + ev = make_node_from_constant(ev, self).optimize(compressor); + return best_of(compressor, ev, self); + } + return self; }); OPT(AST_SymbolRef, function(self, compressor){ @@ -3421,11 +3432,11 @@ merge(Compressor.prototype, { && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { switch (self.name) { case "undefined": - return make_node(AST_Undefined, self).transform(compressor); + return make_node(AST_Undefined, self).optimize(compressor); case "NaN": - return make_node(AST_NaN, self).transform(compressor); + return make_node(AST_NaN, self).optimize(compressor); case "Infinity": - return make_node(AST_Infinity, self).transform(compressor); + return make_node(AST_Infinity, self).optimize(compressor); } } if (compressor.option("evaluate") && compressor.option("reduce_vars")) { @@ -3433,12 +3444,14 @@ merge(Compressor.prototype, { if (d.fixed) { if (d.should_replace === undefined) { var init = d.fixed.evaluate(compressor); - if (init.length > 1) { - var value = init[0].print_to_string().length; + if (init !== d.fixed) { + init = make_node_from_constant(init, d.fixed).optimize(compressor); + init = best_of_expression(init, d.fixed); + var value = init.print_to_string().length; var name = d.name.length; var freq = d.references.length; var overhead = d.global || !freq ? 0 : (name + 2 + value) / freq; - d.should_replace = value <= name + overhead ? init[0] : false; + d.should_replace = value <= name + overhead ? init : false; } else { d.should_replace = false; } @@ -3509,8 +3522,8 @@ merge(Compressor.prototype, { return AST_Seq.cons(car, self); } var cond = self.condition.evaluate(compressor); - if (cond.length > 1) { - if (cond[1]) { + if (cond !== self.condition) { + if (cond) { compressor.warn("Condition always true [{file}:{line},{col}]", self.start); return maintain_this_binding(compressor.parent(), self, self.consequent); } else { @@ -3518,9 +3531,8 @@ merge(Compressor.prototype, { return maintain_this_binding(compressor.parent(), self, self.alternative); } } - var statement = first_in_statement(compressor); - var negated = cond[0].negate(compressor, statement); - if ((statement ? best_of_statement : best_of)(cond[0], negated) === negated) { + var negated = cond.negate(compressor, first_in_statement(compressor)); + if (best_of(compressor, cond, negated) === negated) { self = make_node(AST_Conditional, self, { condition: negated, consequent: self.alternative, @@ -3700,7 +3712,12 @@ merge(Compressor.prototype, { }); } } - return self.evaluate(compressor)[0]; + var ev = self.evaluate(compressor); + if (ev !== self) { + ev = make_node_from_constant(ev, self).optimize(compressor); + return best_of(compressor, ev, self); + } + return self; }); OPT(AST_Dot, function(self, compressor){ @@ -3739,13 +3756,17 @@ merge(Compressor.prototype, { break; } } - return self.evaluate(compressor)[0]; + var ev = self.evaluate(compressor); + if (ev !== self) { + ev = make_node_from_constant(ev, self).optimize(compressor); + return best_of(compressor, ev, self); + } + return self; }); function literals_in_boolean_context(self, compressor) { if (compressor.option("booleans") && compressor.in_boolean_context()) { - var best = first_in_statement(compressor) ? best_of_statement : best_of; - return best(self, make_node(AST_Seq, self, { + return best_of(compressor, self, make_node(AST_Seq, self, { car: self, cdr: make_node(AST_True, self) }).optimize(compressor)); From a80b228d8be37eb6585bca01c6fb5468db5bea42 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 16 Mar 2017 12:03:30 +0800 Subject: [PATCH 04/11] fix `hoist_vars` on `reduce_vars` (#1607) `hoist_vars` converts variable declarations into plain assignments, which then confuses `reduce_vars` fixes #1606 --- lib/compress.js | 6 ++++-- test/compress/reduce_vars.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index b3004fb5..49b618e7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1986,7 +1986,7 @@ merge(Compressor.prototype, { vars.set(def.name.name, def); ++vars_found; }); - var seq = node.to_assignments(); + var seq = node.to_assignments(compressor); var p = tt.parent(); if (p instanceof AST_ForIn && p.init === node) { if (seq == null) { @@ -2579,7 +2579,8 @@ merge(Compressor.prototype, { this.definitions.forEach(function(def){ def.value = null }); }); - AST_Definitions.DEFMETHOD("to_assignments", function(){ + AST_Definitions.DEFMETHOD("to_assignments", function(compressor){ + var reduce_vars = compressor.option("reduce_vars"); var assignments = this.definitions.reduce(function(a, def){ if (def.value) { var name = make_node(AST_SymbolRef, def.name, def.name); @@ -2588,6 +2589,7 @@ merge(Compressor.prototype, { left : name, right : def.value })); + if (reduce_vars) name.definition().fixed = false; } return a; }, []); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index a5ab59f9..bc6c72d4 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1327,3 +1327,27 @@ issue_1595_4: { })(3, 4, 5); } } + +issue_1606: { + options = { + evaluate: true, + hoist_vars: true, + reduce_vars: true, + } + input: { + function f() { + var a; + function g(){}; + var b = 2; + x(b); + } + } + expect: { + function f() { + var a, b; + function g(){}; + b = 2; + x(b); + } + } +} From 5ae04b35452693e886a7f836e62e3290b08016a1 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 16 Mar 2017 13:22:26 +0800 Subject: [PATCH 05/11] make `collapse_vars` consistent with `toplevel` (#1608) fixes #1605 --- lib/compress.js | 4 ++- test/compress/collapse_vars.js | 47 +++++++++++++++++++++++++++++++++- test/compress/issue-973.js | 1 + test/mocha/cli.js | 2 +- test/mocha/minify.js | 1 + 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 49b618e7..dac1f364 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -521,6 +521,7 @@ merge(Compressor.prototype, { var self = compressor.self(); var var_defs_removed = false; + var toplevel = compressor.option("toplevel"); for (var stat_index = statements.length; --stat_index >= 0;) { var stat = statements[stat_index]; if (stat instanceof AST_Definitions) continue; @@ -558,7 +559,8 @@ merge(Compressor.prototype, { // Only interested in cases with just one reference to the variable. var def = self.find_variable && self.find_variable(var_name); - if (!def || !def.references || def.references.length !== 1 || var_name == "arguments") { + if (!def || !def.references || def.references.length !== 1 + || var_name == "arguments" || (!toplevel && def.global)) { side_effects_encountered = true; continue; } diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 6d7e2d9f..558a9ee0 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1152,7 +1152,8 @@ collapse_vars_arguments: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, - keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true, + toplevel:true } input: { var outer = function() { @@ -1335,6 +1336,7 @@ issue_1537: { issue_1562: { options = { collapse_vars: true, + toplevel: true, } input: { var v = 1, B = 2; @@ -1363,3 +1365,46 @@ issue_1562: { for (; f(z + 2) ;) bar(30); } } + +issue_1605_1: { + options = { + collapse_vars: true, + toplevel: false, + } + input: { + function foo(x) { + var y = x; + return y; + } + var o = new Object; + o.p = 1; + } + expect: { + function foo(x) { + return x; + } + var o = new Object; + o.p = 1; + } +} + +issue_1605_2: { + options = { + collapse_vars: true, + toplevel: "vars", + } + input: { + function foo(x) { + var y = x; + return y; + } + var o = new Object; + o.p = 1; + } + expect: { + function foo(x) { + return x; + } + (new Object).p = 1; + } +} diff --git a/test/compress/issue-973.js b/test/compress/issue-973.js index 0e040922..30f886a8 100644 --- a/test/compress/issue-973.js +++ b/test/compress/issue-973.js @@ -50,6 +50,7 @@ this_binding_conditionals: { this_binding_collapse_vars: { options = { collapse_vars: true, + toplevel: true, }; input: { var c = a; c(); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index fa952d91..2b44c901 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -152,7 +152,7 @@ describe("bin/uglifyjs", function () { }); }); it("Should process inline source map", function(done) { - var command = uglifyjscmd + ' test/input/issue-520/input.js -cm toplevel --in-source-map inline --source-map-inline'; + var command = uglifyjscmd + ' test/input/issue-520/input.js -mc toplevel --in-source-map inline --source-map-inline'; exec(command, function (err, stdout) { if (err) throw err; diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 51c46b28..a4587cb7 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -78,6 +78,7 @@ describe("minify", function() { }); it("Should process inline source map", function() { var code = Uglify.minify("./test/input/issue-520/input.js", { + compress: { toplevel: true }, inSourceMap: "inline", sourceMapInline: true }).code + "\n"; From 3563d8c09e36be8f8b9cb9500852778f8d191d5d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 16 Mar 2017 23:20:06 +0800 Subject: [PATCH 06/11] extend `test/run-tests.js` to optionally execute uglified output (#1604) fixes #1588 --- test/compress/issue-1588.js | 87 +++++++++++++++++++++++++++++++++++++ test/run-tests.js | 85 ++++++++++++++++++++++++++++++++---- 2 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 test/compress/issue-1588.js diff --git a/test/compress/issue-1588.js b/test/compress/issue-1588.js new file mode 100644 index 00000000..ff25635c --- /dev/null +++ b/test/compress/issue-1588.js @@ -0,0 +1,87 @@ +screw_ie8: { + options = { + screw_ie8: true, + } + mangle = { + screw_ie8: true, + } + input: { + try { throw "foo"; } catch (x) { console.log(x); } + } + expect_exact: 'try{throw"foo"}catch(o){console.log(o)}' + expect_stdout: [ + "foo" + ] +} + +support_ie8: { + options = { + screw_ie8: false, + } + mangle = { + screw_ie8: false, + } + input: { + try { throw "foo"; } catch (x) { console.log(x); } + } + expect_exact: 'try{throw"foo"}catch(x){console.log(x)}' + expect_stdout: "foo" +} + +safe_undefined: { + options = { + conditionals: true, + if_return: true, + unsafe: false, + } + mangle = {} + input: { + var a, c; + console.log(function(undefined) { + return function() { + if (a) + return b; + if (c) + return d; + }; + }(1)()); + } + expect: { + var a, c; + console.log(function(n) { + return function() { + return a ? b : c ? d : void 0; + }; + }(1)()); + } + expect_stdout: true +} + +unsafe_undefined: { + options = { + conditionals: true, + if_return: true, + unsafe: true, + } + mangle = {} + input: { + var a, c; + console.log(function(undefined) { + return function() { + if (a) + return b; + if (c) + return d; + }; + }()()); + } + expect: { + var a, c; + console.log(function(n) { + return function() { + return a ? b : c ? d : n; + }; + }()()); + } + expect_stdout: true +} diff --git a/test/run-tests.js b/test/run-tests.js index 36d26ef7..898bb793 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -6,6 +6,7 @@ var U = require("../tools/node"); var path = require("path"); var fs = require("fs"); var assert = require("assert"); +var vm = require("vm"); var tests_dir = path.dirname(module.filename); var failures = 0; @@ -165,6 +166,51 @@ function run_compress_tests() { failed_files[file] = 1; } } + if (test.expect_stdout) { + try { + var stdout = run_code(input_code); + if (test.expect_stdout === true) { + test.expect_stdout = stdout; + } + if (test.expect_stdout != stdout) { + log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED STDOUT---\n{expected}\n---ACTUAL STDOUT---\n{actual}\n\n", { + input: input_code, + expected: test.expect_stdout, + actual: stdout, + }); + failures++; + failed_files[file] = 1; + } else { + try { + stdout = run_code(output); + if (test.expect_stdout != stdout) { + log("!!! failed\n---INPUT---\n{input}\n---EXPECTED STDOUT---\n{expected}\n---ACTUAL STDOUT---\n{actual}\n\n", { + input: input_code, + expected: test.expect_stdout, + actual: stdout, + }); + failures++; + failed_files[file] = 1; + } + } catch (ex) { + log("!!! Execution of output failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--ERROR--\n{error}\n\n", { + input: input_code, + output: output, + error: ex.toString(), + }); + failures++; + failed_files[file] = 1; + } + } + } catch (ex) { + log("!!! Execution of input failed\n---INPUT---\n{input}\n--ERROR--\n{error}\n\n", { + input: input_code, + error: ex.toString(), + }); + failures++; + failed_files[file] = 1; + } + } } } var tests = parse_test(path.resolve(dir, file)); @@ -215,9 +261,9 @@ function parse_test(file) { } function read_string(stat) { - if (stat.TYPE === "SimpleStatement") { + if (stat.TYPE == "SimpleStatement") { var body = stat.body; - out: switch(body.TYPE) { + switch(body.TYPE) { case "String": return body.value; case "Array": @@ -243,12 +289,13 @@ function parse_test(file) { return true; } if (node instanceof U.AST_LabeledStatement) { + var label = node.label; assert.ok( - ["input", "expect", "expect_exact", "expect_warnings"].indexOf(node.label.name) >= 0, + ["input", "expect", "expect_exact", "expect_warnings", "expect_stdout"].indexOf(label.name) >= 0, tmpl("Unsupported label {name} [{line},{col}]", { - name: node.label.name, - line: node.label.start.line, - col: node.label.start.col + name: label.name, + line: label.start.line, + col: label.start.col }) ); var stat = node.body; @@ -256,10 +303,16 @@ function parse_test(file) { if (stat.body.length == 1) stat = stat.body[0]; else if (stat.body.length == 0) stat = new U.AST_EmptyStatement(); } - if (node.label.name === "expect_exact") { - test[node.label.name] = read_string(stat); + if (label.name == "expect_exact") { + test[label.name] = read_string(stat); + } else if (label.name == "expect_stdout") { + if (stat.TYPE == "SimpleStatement" && stat.body instanceof U.AST_Boolean) { + test[label.name] = stat.body.value; + } else { + test[label.name] = read_string(stat) + "\n"; + } } else { - test[node.label.name] = stat; + test[label.name] = stat; } return true; } @@ -281,3 +334,17 @@ function evaluate(code) { code = make_code(code, { beautify: true }); return new Function("return(" + code + ")")(); } + +function run_code(code) { + var stdout = ""; + var original_write = process.stdout.write; + process.stdout.write = function(chunk) { + stdout += chunk; + }; + try { + new vm.Script(code).runInNewContext({ console: console }, { timeout: 5000 }); + return stdout; + } finally { + process.stdout.write = original_write; + } +} From ac403018135b0ba700ef6223970c1bbc2a518107 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 17 Mar 2017 00:26:48 +0800 Subject: [PATCH 07/11] fix chained evaluation (#1610) `reduce_vars` enables substitution of variables but did not clone the value's `AST_Node`. This confuses `collapse_vars` and result in invalid AST and subsequent crash. fixes #1609 --- lib/compress.js | 2 +- test/compress/issue-1609.js | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1609.js diff --git a/lib/compress.js b/lib/compress.js index dac1f364..c3f12549 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3461,7 +3461,7 @@ merge(Compressor.prototype, { } } if (d.should_replace) { - return d.should_replace; + return d.should_replace.clone(true); } } } diff --git a/test/compress/issue-1609.js b/test/compress/issue-1609.js new file mode 100644 index 00000000..577a3ee1 --- /dev/null +++ b/test/compress/issue-1609.js @@ -0,0 +1,56 @@ +chained_evaluation_1: { + options = { + collapse_vars: true, + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + (function() { + var a = 1; + (function() { + var b = a, c; + c = f(b); + c.bar = b; + })(); + })(); + } + expect: { + (function() { + (function() { + var c; + c = f(1); + c.bar = 1; + })(); + })(); + } +} + +chained_evaluation_2: { + options = { + collapse_vars: true, + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + (function() { + var a = "long piece of string"; + (function() { + var b = a, c; + c = f(b); + c.bar = b; + })(); + })(); + } + expect: { + (function() { + var a = "long piece of string"; + (function() { + var c; + c = f(a); + c.bar = a; + })(); + })(); + } +} From b2b8a0d386ac5e38e17212c734914cde1f3eee83 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 17 Mar 2017 02:01:33 +0800 Subject: [PATCH 08/11] v2.8.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ba24c7c..34b1674a 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.12", + "version": "2.8.13", "engines": { "node": ">=0.8.0" }, From b7c112eefe4b7840cefd85287e3f858784c56a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Maughan=20Tegn=C3=A9r?= Date: Thu, 16 Mar 2017 20:08:38 +0100 Subject: [PATCH 09/11] Add `--in-source-map inline` documentation (#1611) --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a33e0b32..396f9a94 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,8 @@ The available options are: --source-map-inline Write base64-encoded source map to the end of js output. --in-source-map Input source map, useful if you're compressing JS that was generated from some other original - code. + code. Specify "inline" if the source map is included + inline with the sources. --screw-ie8 Use this flag if you don't wish to support Internet Explorer 6/7/8. By default UglifyJS will not try to be IE-proof. @@ -200,9 +201,10 @@ compressed JS by mapping every token in the compiled JS to its original location. To use this feature you need to pass `--in-source-map -/path/to/input/source.map`. Normally the input source map should also point -to the file containing the generated JS, so if that's correct you can omit -input files from the command line. +/path/to/input/source.map` or `--in-source-map inline` if the source map is +included inline with the sources. Normally the input source map should also +point to the file containing the generated JS, so if that's correct you can +omit input files from the command line. ## Mangler options From fb092839c26ddaa0614df5295be85ea207bda9f7 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 18 Mar 2017 01:56:15 +0800 Subject: [PATCH 10/11] fix top-level directives in compress tests (#1615) `input` and `expect` are parsed as `AST_BlockStatement` which does not support `AST_Directive` by default. Emulate that by transforming preceding `AST_SimpleStatement`s of `AST_String` into `AST_Directive`. --- test/compress/issue-1202.js | 1 - test/compress/screw-ie8.js | 31 ++++++++++++++++++++----------- test/run-tests.js | 17 +++++++++-------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/test/compress/issue-1202.js b/test/compress/issue-1202.js index 136515fd..27bc4248 100644 --- a/test/compress/issue-1202.js +++ b/test/compress/issue-1202.js @@ -49,4 +49,3 @@ mangle_keep_fnames_true: { } } } - diff --git a/test/compress/screw-ie8.js b/test/compress/screw-ie8.js index 4fbb95c8..0fb68c25 100644 --- a/test/compress/screw-ie8.js +++ b/test/compress/screw-ie8.js @@ -1,20 +1,29 @@ do_screw: { - options = { screw_ie8: true }; + options = { + screw_ie8: true, + } beautify = { screw_ie8: true, - ascii_only: true - }; - - input: f("\v"); - expect_exact: 'f("\\v");'; + ascii_only: true, + } + input: { + f("\v"); + } + expect_exact: 'f("\\v");' } dont_screw: { - options = { screw_ie8: false }; - beautify = { screw_ie8: false, ascii_only: true }; - - input: f("\v"); - expect_exact: 'f("\\x0B");'; + options = { + screw_ie8: false, + } + beautify = { + screw_ie8: false, + ascii_only: true, + } + input: { + f("\v"); + } + expect_exact: 'f("\\x0B");' } do_screw_constants: { diff --git a/test/run-tests.js b/test/run-tests.js index 898bb793..c3c142d1 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -72,10 +72,15 @@ function test_directory(dir) { } function as_toplevel(input, mangle_options) { - if (input instanceof U.AST_BlockStatement) input = input.body; - else if (input instanceof U.AST_Statement) input = [ input ]; - else throw new Error("Unsupported input syntax"); - var toplevel = new U.AST_Toplevel({ body: input }); + if (!(input instanceof U.AST_BlockStatement)) + throw new Error("Unsupported input syntax"); + for (var i = 0; i < input.body.length; i++) { + var stat = input.body[i]; + if (stat instanceof U.AST_SimpleStatement && stat.body instanceof U.AST_String) + input.body[i] = new U.AST_Directive(stat.body); + else break; + } + var toplevel = new U.AST_Toplevel(input); toplevel.figure_out_scope(mangle_options); return toplevel; } @@ -299,10 +304,6 @@ function parse_test(file) { }) ); var stat = node.body; - if (stat instanceof U.AST_BlockStatement) { - if (stat.body.length == 1) stat = stat.body[0]; - else if (stat.body.length == 0) stat = new U.AST_EmptyStatement(); - } if (label.name == "expect_exact") { test[label.name] = read_string(stat); } else if (label.name == "expect_stdout") { From 0489d6de6499154c758a6464387546ec5a060f67 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 18 Mar 2017 02:33:51 +0800 Subject: [PATCH 11/11] handle runtime errors in `expect_stdout` (#1618) allow test to pass if both `input` and `expect` throws the same kind of error --- test/compress/issue-1588.js | 12 ++++++++ test/run-tests.js | 61 ++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/test/compress/issue-1588.js b/test/compress/issue-1588.js index ff25635c..fce9ba54 100644 --- a/test/compress/issue-1588.js +++ b/test/compress/issue-1588.js @@ -85,3 +85,15 @@ unsafe_undefined: { } expect_stdout: true } + +runtime_error: { + input: { + const a = 1; + console.log(a++); + } + expect: { + const a = 1; + console.log(a++); + } + expect_stdout: true +} diff --git a/test/run-tests.js b/test/run-tests.js index c3c142d1..7f3256e0 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -172,48 +172,33 @@ function run_compress_tests() { } } if (test.expect_stdout) { - try { - var stdout = run_code(input_code); - if (test.expect_stdout === true) { - test.expect_stdout = stdout; - } - if (test.expect_stdout != stdout) { - log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED STDOUT---\n{expected}\n---ACTUAL STDOUT---\n{actual}\n\n", { + 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, + 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 = 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, + 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 { - try { - stdout = run_code(output); - if (test.expect_stdout != stdout) { - log("!!! failed\n---INPUT---\n{input}\n---EXPECTED STDOUT---\n{expected}\n---ACTUAL STDOUT---\n{actual}\n\n", { - input: input_code, - expected: test.expect_stdout, - actual: stdout, - }); - failures++; - failed_files[file] = 1; - } - } catch (ex) { - log("!!! Execution of output failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--ERROR--\n{error}\n\n", { - input: input_code, - output: output, - error: ex.toString(), - }); - failures++; - failed_files[file] = 1; - } } - } catch (ex) { - log("!!! Execution of input failed\n---INPUT---\n{input}\n--ERROR--\n{error}\n\n", { - input: input_code, - error: ex.toString(), - }); - failures++; - failed_files[file] = 1; } } } @@ -345,7 +330,13 @@ function run_code(code) { try { new vm.Script(code).runInNewContext({ console: console }, { timeout: 5000 }); return stdout; + } catch (ex) { + return ex; } finally { process.stdout.write = original_write; } } + +function same_stdout(expected, actual) { + return typeof expected == typeof actual && expected.toString() == actual.toString(); +}